mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-09-27 20:21:59 +02:00
feat(rss): generate rss feed from podcast entity
- refactor episode, podcast and category entities to add dynamic properties - refactor Routes when adding feed route - update migration files to better fit itunes' and rss' specs - update podcast and episode forms - add SimpleRSSElement class to Libraries - add rss_helper - update home controller to redirect if system has only one podcast
This commit is contained in:
parent
d31191732e
commit
c815ecd664
@ -100,7 +100,7 @@ class App extends BaseConfig
|
|||||||
| dates with the date helper, and can be retrieved through app_timezone()
|
| dates with the date helper, and can be retrieved through app_timezone()
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
public $appTimezone = 'America/Chicago';
|
public $appTimezone = 'UTC';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -22,7 +22,7 @@ $routes->setDefaultMethod('index');
|
|||||||
$routes->setTranslateURIDashes(false);
|
$routes->setTranslateURIDashes(false);
|
||||||
$routes->set404Override();
|
$routes->set404Override();
|
||||||
$routes->setAutoRoute(false);
|
$routes->setAutoRoute(false);
|
||||||
$routes->addPlaceholder('podcastSlug', '@[a-z0-9\_]{1,191}');
|
$routes->addPlaceholder('podcastName', '[a-z0-9\_]{1,191}');
|
||||||
$routes->addPlaceholder('episodeSlug', '[a-z0-9\-]{1,191}');
|
$routes->addPlaceholder('episodeSlug', '[a-z0-9\-]{1,191}');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,24 +34,27 @@ $routes->addPlaceholder('episodeSlug', '[a-z0-9\-]{1,191}');
|
|||||||
// We get a performance increase by specifying the default
|
// We get a performance increase by specifying the default
|
||||||
// route since we don't have to scan directories.
|
// route since we don't have to scan directories.
|
||||||
$routes->get('/', 'Home::index', ['as' => 'home']);
|
$routes->get('/', 'Home::index', ['as' => 'home']);
|
||||||
$routes->add('new-podcast', 'Podcasts::create', ['as' => 'podcasts_create']);
|
$routes->add('new-podcast', 'Podcast::create', ['as' => 'podcast_create']);
|
||||||
|
|
||||||
$routes->group('(:podcastSlug)', function ($routes) {
|
$routes->group('@(:podcastName)', function ($routes) {
|
||||||
$routes->add('/', 'Podcasts::view/$1', ['as' => 'podcasts_view']);
|
$routes->add('/', 'Podcast::view/$1', ['as' => 'podcast_view']);
|
||||||
$routes->add('new-episode', 'Episodes::create/$1', [
|
$routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
|
||||||
'as' => 'episodes_create',
|
$routes->add('new-episode', 'Episode::create/$1', [
|
||||||
|
'as' => 'episode_create',
|
||||||
]);
|
]);
|
||||||
$routes->add('(:episodeSlug)', 'Episodes::view/$1/$2', [
|
$routes->add('episodes/(:episodeSlug)', 'Episode::view/$1/$2', [
|
||||||
'as' => 'episodes_view',
|
'as' => 'episode_view',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
|
// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
|
||||||
$routes->add('/stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3');
|
$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
|
||||||
|
'as' => 'analytics_hit',
|
||||||
|
]);
|
||||||
|
|
||||||
// Show the Unknown UserAgents
|
// Show the Unknown UserAgents
|
||||||
$routes->add('/.well-known/unknown-useragents', 'UnknownUserAgents');
|
$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents');
|
||||||
$routes->add('/.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
|
$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------
|
* --------------------------------------------------------------------
|
||||||
|
@ -21,7 +21,7 @@ class Toolbar extends BaseConfig
|
|||||||
\CodeIgniter\Debug\Toolbar\Collectors\Database::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Database::class,
|
||||||
\CodeIgniter\Debug\Toolbar\Collectors\Logs::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Logs::class,
|
||||||
\CodeIgniter\Debug\Toolbar\Collectors\Views::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Views::class,
|
||||||
// \CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
|
||||||
\CodeIgniter\Debug\Toolbar\Collectors\Files::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Files::class,
|
||||||
\CodeIgniter\Debug\Toolbar\Collectors\Routes::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Routes::class,
|
||||||
\CodeIgniter\Debug\Toolbar\Collectors\Events::class,
|
\CodeIgniter\Debug\Toolbar\Collectors\Events::class,
|
||||||
|
@ -50,7 +50,7 @@ class BaseController extends Controller
|
|||||||
set_user_session_referer();
|
set_user_session_referer();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function stats($postcast_id)
|
protected static function triggerWebpageHit($postcast_id)
|
||||||
{
|
{
|
||||||
webpage_hit($postcast_id);
|
webpage_hit($postcast_id);
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,17 @@ namespace App\Controllers;
|
|||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
|
|
||||||
class Episodes extends BaseController
|
helper('podcast');
|
||||||
|
|
||||||
|
class Episode extends BaseController
|
||||||
{
|
{
|
||||||
public function create($podcast_slug)
|
public function create($podcast_name)
|
||||||
{
|
{
|
||||||
helper(['form', 'database', 'media', 'id3']);
|
helper(['form', 'database', 'media', 'id3']);
|
||||||
|
|
||||||
$episode_model = new EpisodeModel();
|
$episode_model = new EpisodeModel();
|
||||||
$podcast_model = new PodcastModel();
|
$podcast_model = new PodcastModel();
|
||||||
$podcast_name = substr($podcast_slug, 1);
|
|
||||||
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -33,13 +35,9 @@ class Episodes extends BaseController
|
|||||||
) {
|
) {
|
||||||
$data = [
|
$data = [
|
||||||
'podcast' => $podcast,
|
'podcast' => $podcast,
|
||||||
'episode_types' => field_enums(
|
|
||||||
$episode_model->prefixTable('episodes'),
|
|
||||||
'type'
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
echo view('episodes/create', $data);
|
echo view('episode/create', $data);
|
||||||
} else {
|
} else {
|
||||||
$episode_slug = $this->request->getVar('slug');
|
$episode_slug = $this->request->getVar('slug');
|
||||||
|
|
||||||
@ -49,7 +47,7 @@ class Episodes extends BaseController
|
|||||||
$image = $this->request->getFile('image');
|
$image = $this->request->getFile('image');
|
||||||
|
|
||||||
// By default, the episode's image path is set to the podcast's
|
// By default, the episode's image path is set to the podcast's
|
||||||
$image_path = $podcast->image;
|
$image_path = $podcast->image_uri;
|
||||||
|
|
||||||
// check whether the user has inputted an image and store it
|
// check whether the user has inputted an image and store it
|
||||||
if ($image->isValid()) {
|
if ($image->isValid()) {
|
||||||
@ -81,20 +79,21 @@ class Episodes extends BaseController
|
|||||||
'podcast_id' => $podcast->id,
|
'podcast_id' => $podcast->id,
|
||||||
'title' => $this->request->getVar('title'),
|
'title' => $this->request->getVar('title'),
|
||||||
'slug' => $episode_slug,
|
'slug' => $episode_slug,
|
||||||
'enclosure_url' => $episode_path,
|
'enclosure_uri' => $episode_path,
|
||||||
'enclosure_length' => $episode_file->getSize(),
|
'enclosure_length' => $episode_file->getSize(),
|
||||||
'enclosure_type' => $episode_file_metadata['mime_type'],
|
'enclosure_type' => $episode_file_metadata['mime_type'],
|
||||||
'guid' => $podcast_slug . '/' . $episode_slug,
|
|
||||||
'pub_date' => $this->request->getVar('pub_date'),
|
'pub_date' => $this->request->getVar('pub_date'),
|
||||||
'description' => $this->request->getVar('description'),
|
'description' => $this->request->getVar('description'),
|
||||||
'duration' => $episode_file_metadata['playtime_seconds'],
|
'duration' => $episode_file_metadata['playtime_seconds'],
|
||||||
'image' => $image_path,
|
'image_uri' => $image_path,
|
||||||
'explicit' => $this->request->getVar('explicit') or false,
|
'explicit' => $this->request->getVar('explicit') or false,
|
||||||
'number' => $this->request->getVar('episode_number'),
|
'number' => $this->request->getVar('episode_number'),
|
||||||
'season_number' => $this->request->getVar('season_number')
|
'season_number' => $this->request->getVar('season_number')
|
||||||
? $this->request->getVar('season_number')
|
? $this->request->getVar('season_number')
|
||||||
: null,
|
: null,
|
||||||
'type' => $this->request->getVar('type'),
|
'type' => $this->request->getVar('type'),
|
||||||
|
'author_name' => $this->request->getVar('author_name'),
|
||||||
|
'author_email' => $this->request->getVar('author_email'),
|
||||||
'block' => $this->request->getVar('block') or false,
|
'block' => $this->request->getVar('block') or false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -103,30 +102,25 @@ class Episodes extends BaseController
|
|||||||
$episode_file = write_file_tags($podcast, $episode);
|
$episode_file = write_file_tags($podcast, $episode);
|
||||||
|
|
||||||
return redirect()->to(
|
return redirect()->to(
|
||||||
base_url(
|
base_url(route_to('episode_view', $podcast_name, $episode_slug))
|
||||||
route_to(
|
|
||||||
'episodes_view',
|
|
||||||
'/@' . $podcast_name,
|
|
||||||
$episode_slug
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view($podcast_slug, $episode_slug)
|
public function view($podcast_name, $episode_slug)
|
||||||
{
|
{
|
||||||
$podcast_model = new PodcastModel();
|
$podcast_model = new PodcastModel();
|
||||||
$episode_model = new EpisodeModel();
|
$episode_model = new EpisodeModel();
|
||||||
|
|
||||||
$data = [
|
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
||||||
'podcast' => $podcast_model
|
$episode = $episode_model->where('slug', $episode_slug)->first();
|
||||||
->where('name', substr($podcast_slug, 1))
|
|
||||||
->first(),
|
|
||||||
'episode' => $episode_model->where('slug', $episode_slug)->first(),
|
|
||||||
];
|
|
||||||
self::stats($data['podcast']->id);
|
|
||||||
|
|
||||||
return view('episodes/view.php', $data);
|
$data = [
|
||||||
|
'podcast' => $podcast,
|
||||||
|
'episode' => $episode,
|
||||||
|
];
|
||||||
|
self::triggerWebpageHit($data['podcast']->id);
|
||||||
|
|
||||||
|
return view('episode/view.php', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
22
app/Controllers/Feed.php
Normal file
22
app/Controllers/Feed.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\PodcastModel;
|
||||||
|
use CodeIgniter\Controller;
|
||||||
|
|
||||||
|
class Feed extends Controller
|
||||||
|
{
|
||||||
|
public function index($podcast_name)
|
||||||
|
{
|
||||||
|
helper('rss');
|
||||||
|
|
||||||
|
$podcast_model = new PodcastModel();
|
||||||
|
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
||||||
|
|
||||||
|
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||||
|
$this->cachePage(DECADE);
|
||||||
|
|
||||||
|
return $this->response->setXML(get_rss_feed($podcast));
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,18 @@ class Home extends BaseController
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$model = new PodcastModel();
|
$model = new PodcastModel();
|
||||||
$data = ['podcasts' => $model->findAll()];
|
|
||||||
|
|
||||||
|
$all_podcasts = $model->findAll();
|
||||||
|
|
||||||
|
// check if there's only one podcast to redirect user to it
|
||||||
|
if (count($all_podcasts) == 1) {
|
||||||
|
return redirect()->to(
|
||||||
|
base_url(route_to('podcast_view', $all_podcasts[0]->name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default behavior: list all podcasts on home page
|
||||||
|
$data = ['podcasts' => $all_podcasts];
|
||||||
return view('home', $data);
|
return view('home', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,14 @@
|
|||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Entities\Podcast;
|
|
||||||
use App\Models\CategoryModel;
|
use App\Models\CategoryModel;
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
use App\Models\LanguageModel;
|
use App\Models\LanguageModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
|
|
||||||
class Podcasts extends BaseController
|
class Podcast extends BaseController
|
||||||
{
|
{
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
@ -39,30 +37,27 @@ class Podcasts extends BaseController
|
|||||||
'browser_lang' => get_browser_language(
|
'browser_lang' => get_browser_language(
|
||||||
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
|
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
|
||||||
),
|
),
|
||||||
'podcast_types' => field_enums(
|
|
||||||
$podcast_model->prefixTable('podcasts'),
|
|
||||||
'type'
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
echo view('podcasts/create', $data);
|
echo view('podcast/create', $data);
|
||||||
} else {
|
} else {
|
||||||
$image = $this->request->getFile('image');
|
$image = $this->request->getFile('image');
|
||||||
$podcast_name = $this->request->getVar('name');
|
$podcast_name = $this->request->getVar('name');
|
||||||
$image_path = save_podcast_media($image, $podcast_name, 'cover');
|
$image_path = save_podcast_media($image, $podcast_name, 'cover');
|
||||||
|
|
||||||
$podcast = new Podcast([
|
$podcast = new \App\Entities\Podcast([
|
||||||
'title' => $this->request->getVar('title'),
|
'title' => $this->request->getVar('title'),
|
||||||
'name' => $podcast_name,
|
'name' => $podcast_name,
|
||||||
'description' => $this->request->getVar('description'),
|
'description' => $this->request->getVar('description'),
|
||||||
'episode_description_footer' => $this->request->getVar(
|
'episode_description_footer' => $this->request->getVar(
|
||||||
'episode_description_footer'
|
'episode_description_footer'
|
||||||
),
|
),
|
||||||
'image' => $image_path,
|
'image_uri' => $image_path,
|
||||||
'language' => $this->request->getVar('language'),
|
'language' => $this->request->getVar('language'),
|
||||||
'category' => $this->request->getVar('category'),
|
'category' => $this->request->getVar('category'),
|
||||||
'explicit' => $this->request->getVar('explicit') or false,
|
'explicit' => $this->request->getVar('explicit') or false,
|
||||||
'author' => $this->request->getVar('author'),
|
'author_name' => $this->request->getVar('author_name'),
|
||||||
|
'author_email' => $this->request->getVar('author_email'),
|
||||||
'owner_name' => $this->request->getVar('owner_name'),
|
'owner_name' => $this->request->getVar('owner_name'),
|
||||||
'owner_email' => $this->request->getVar('owner_email'),
|
'owner_email' => $this->request->getVar('owner_email'),
|
||||||
'type' => $this->request->getVar('type'),
|
'type' => $this->request->getVar('type'),
|
||||||
@ -77,18 +72,16 @@ class Podcasts extends BaseController
|
|||||||
$podcast_model->save($podcast);
|
$podcast_model->save($podcast);
|
||||||
|
|
||||||
return redirect()->to(
|
return redirect()->to(
|
||||||
base_url(route_to('podcasts_view', '@' . $podcast_name))
|
base_url(route_to('podcast_view', $podcast->name))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view($slug)
|
public function view($podcast_name)
|
||||||
{
|
{
|
||||||
$podcast_model = new PodcastModel();
|
$podcast_model = new PodcastModel();
|
||||||
$episode_model = new EpisodeModel();
|
$episode_model = new EpisodeModel();
|
||||||
|
|
||||||
$podcast_name = substr($slug, 1);
|
|
||||||
|
|
||||||
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
$podcast = $podcast_model->where('name', $podcast_name)->first();
|
||||||
$data = [
|
$data = [
|
||||||
'podcast' => $podcast,
|
'podcast' => $podcast,
|
||||||
@ -96,8 +89,8 @@ class Podcasts extends BaseController
|
|||||||
->where('podcast_id', $podcast->id)
|
->where('podcast_id', $podcast->id)
|
||||||
->findAll(),
|
->findAll(),
|
||||||
];
|
];
|
||||||
self::stats($podcast->id);
|
self::triggerWebpageHit($podcast->id);
|
||||||
|
|
||||||
return view('podcasts/view', $data);
|
return view('podcast/view', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,8 +29,7 @@ class AddCategories extends Migration
|
|||||||
],
|
],
|
||||||
'code' => [
|
'code' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 1024,
|
'constraint' => 191,
|
||||||
'unique' => true,
|
|
||||||
],
|
],
|
||||||
'apple_category' => [
|
'apple_category' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
@ -42,6 +41,7 @@ class AddCategories extends Migration
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addKey('id', true);
|
||||||
|
$this->forge->addUniqueKey('code');
|
||||||
$this->forge->addForeignKey('parent_id', 'categories', 'id');
|
$this->forge->addForeignKey('parent_id', 'categories', 'id');
|
||||||
$this->forge->createTable('categories');
|
$this->forge->createTable('categories');
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class AddPodcasts extends Migration
|
|||||||
'comment' =>
|
'comment' =>
|
||||||
'The show description. Where description is text containing one or more sentences describing your podcast to potential listeners. The maximum amount of text allowed for this tag is 4000 characters. To include links in your description or rich HTML, adhere to the following technical guidelines: enclose all portions of your XML that contain embedded HTML in a CDATA section to prevent formatting issues, and to ensure proper link functionality.',
|
'The show description. Where description is text containing one or more sentences describing your podcast to potential listeners. The maximum amount of text allowed for this tag is 4000 characters. To include links in your description or rich HTML, adhere to the following technical guidelines: enclose all portions of your XML that contain embedded HTML in a CDATA section to prevent formatting issues, and to ensure proper link functionality.',
|
||||||
],
|
],
|
||||||
'image' => [
|
'image_uri' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 1024,
|
'constraint' => 1024,
|
||||||
'comment' =>
|
'comment' =>
|
||||||
@ -67,11 +67,18 @@ class AddPodcasts extends Migration
|
|||||||
'comment' =>
|
'comment' =>
|
||||||
'The podcast parental advisory information. The explicit value can be one of the following: True: If you specify true, indicating the presence of explicit content, Apple Podcasts displays an Explicit parental advisory graphic for your podcast. Podcasts containing explicit material aren’t available in some Apple Podcasts territories. False: If you specify false, indicating that your podcast doesn’t contain explicit language or adult content, Apple Podcasts displays a Clean parental advisory graphic for your podcast.',
|
'The podcast parental advisory information. The explicit value can be one of the following: True: If you specify true, indicating the presence of explicit content, Apple Podcasts displays an Explicit parental advisory graphic for your podcast. Podcasts containing explicit material aren’t available in some Apple Podcasts territories. False: If you specify false, indicating that your podcast doesn’t contain explicit language or adult content, Apple Podcasts displays a Clean parental advisory graphic for your podcast.',
|
||||||
],
|
],
|
||||||
'author' => [
|
'author_name' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 1024,
|
'constraint' => 1024,
|
||||||
'comment' =>
|
'comment' =>
|
||||||
'The group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.',
|
'Name of the group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'author_email' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 1024,
|
||||||
|
'owner_email' =>
|
||||||
|
'Email of the group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.',
|
||||||
'null' => true,
|
'null' => true,
|
||||||
],
|
],
|
||||||
'owner_name' => [
|
'owner_name' => [
|
||||||
|
@ -41,11 +41,11 @@ class AddEpisodes extends Migration
|
|||||||
'constraint' => 191,
|
'constraint' => 191,
|
||||||
'comment' => 'Episode slug for URLs',
|
'comment' => 'Episode slug for URLs',
|
||||||
],
|
],
|
||||||
'enclosure_url' => [
|
'enclosure_uri' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 1024,
|
'constraint' => 1024,
|
||||||
'comment' =>
|
'comment' =>
|
||||||
'The URL attribute points to your podcast media file. The file extension specified within the URL attribute determines whether or not content appears in the podcast directory. Supported file formats include M4A, MP3, MOV, MP4, M4V, and PDF.',
|
'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' => [
|
'enclosure_length' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
@ -69,7 +69,7 @@ class AddEpisodes extends Migration
|
|||||||
'pub_date' => [
|
'pub_date' => [
|
||||||
'type' => 'DATETIME',
|
'type' => 'DATETIME',
|
||||||
'comment' =>
|
'comment' =>
|
||||||
'The date and time when an episode was released. Format the date using the RFC 2822 specifications. For example: Wed, 15 Jun 2019 19:00:00 GMT.',
|
'The date and time when an episode was released. Format the date using the RFC 2822 specifications. For example: Wed, 15 Jun 2019 19:00:00 UTC.',
|
||||||
],
|
],
|
||||||
'description' => [
|
'description' => [
|
||||||
'type' => 'TEXT',
|
'type' => 'TEXT',
|
||||||
@ -84,7 +84,7 @@ class AddEpisodes extends Migration
|
|||||||
'comment' =>
|
'comment' =>
|
||||||
'The duration of an episode. Different duration formats are accepted however it is recommended to convert the length of the episode into seconds.',
|
'The duration of an episode. Different duration formats are accepted however it is recommended to convert the length of the episode into seconds.',
|
||||||
],
|
],
|
||||||
'image' => [
|
'image_uri' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 1024,
|
'constraint' => 1024,
|
||||||
'comment' =>
|
'comment' =>
|
||||||
@ -112,6 +112,20 @@ class AddEpisodes extends Migration
|
|||||||
'comment' =>
|
'comment' =>
|
||||||
'The episode season number. If an episode is within a season use this tag. Where season is a non-zero integer (1, 2, 3, etc.) representing your season number. To allow the season feature for shows containing a single season, if only one season exists in the RSS feed, Apple Podcasts doesn’t display a season number. When you add a second season to the RSS feed, Apple Podcasts displays the season numbers.',
|
'The episode season number. If an episode is within a season use this tag. Where season is a non-zero integer (1, 2, 3, etc.) representing your season number. To allow the season feature for shows containing a single season, if only one season exists in the RSS feed, Apple Podcasts doesn’t display a season number. When you add a second season to the RSS feed, Apple Podcasts displays the season numbers.',
|
||||||
],
|
],
|
||||||
|
'author_name' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 1024,
|
||||||
|
'comment' =>
|
||||||
|
'Name of the group responsible for creating the episode. Episode author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all episodes created by the same entity.',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'author_email' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 1024,
|
||||||
|
'owner_email' =>
|
||||||
|
'Email of the group responsible for creating the episode. Episode author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all episodes created by the same entity.',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
'type' => [
|
'type' => [
|
||||||
'type' => 'ENUM',
|
'type' => 'ENUM',
|
||||||
'constraint' => ['full', 'trailer', 'bonus'],
|
'constraint' => ['full', 'trailer', 'bonus'],
|
||||||
|
@ -7,13 +7,27 @@
|
|||||||
|
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
|
use App\Models\CategoryModel;
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class Category extends Entity
|
class Category extends Entity
|
||||||
{
|
{
|
||||||
|
protected $parent;
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'parent_id' => 'integer',
|
'parent_id' => 'integer',
|
||||||
|
'code' => 'string',
|
||||||
'apple_category' => 'string',
|
'apple_category' => 'string',
|
||||||
'google_category' => 'string',
|
'google_category' => 'string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
$category_model = new CategoryModel();
|
||||||
|
$parent_id = $this->attributes['parent_id'];
|
||||||
|
|
||||||
|
return $parent_id != 0
|
||||||
|
? $category_model->find($this->attributes['parent_id'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,25 +7,85 @@
|
|||||||
|
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class Episode extends 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 $casts = [
|
protected $casts = [
|
||||||
'slug' => 'string',
|
'slug' => 'string',
|
||||||
'title' => 'string',
|
'title' => 'string',
|
||||||
'enclosure_url' => 'string',
|
'enclosure_uri' => 'string',
|
||||||
'enclosure_length' => 'integer',
|
'enclosure_length' => 'integer',
|
||||||
'enclosure_type' => 'string',
|
'enclosure_type' => 'string',
|
||||||
'guid' => 'string',
|
|
||||||
'pub_date' => 'datetime',
|
'pub_date' => 'datetime',
|
||||||
'description' => 'string',
|
'description' => 'string',
|
||||||
'duration' => 'integer',
|
'duration' => 'integer',
|
||||||
'image' => 'string',
|
'image_uri' => 'string',
|
||||||
|
'author_name' => '?string',
|
||||||
|
'author_email' => '?string',
|
||||||
'explicit' => 'boolean',
|
'explicit' => 'boolean',
|
||||||
'number' => 'integer',
|
'number' => 'integer',
|
||||||
'season_number' => '?integer',
|
'season_number' => '?integer',
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'block' => 'boolean',
|
'block' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getImageMediaPath()
|
||||||
|
{
|
||||||
|
return media_path($this->attributes['image_uri']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImageUrl()
|
||||||
|
{
|
||||||
|
return media_url($this->attributes['image_uri']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnclosureMediaPath()
|
||||||
|
{
|
||||||
|
return media_path($this->attributes['enclosure_uri']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnclosureUrl()
|
||||||
|
{
|
||||||
|
return base_url(
|
||||||
|
route_to(
|
||||||
|
'analytics_hit',
|
||||||
|
$this->attributes['podcast_id'],
|
||||||
|
$this->attributes['id'],
|
||||||
|
$this->attributes['enclosure_uri']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLink()
|
||||||
|
{
|
||||||
|
return base_url(
|
||||||
|
route_to(
|
||||||
|
'episode_view',
|
||||||
|
$this->getPodcast()->name,
|
||||||
|
$this->attributes['slug']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGuid()
|
||||||
|
{
|
||||||
|
return $this->getLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPodcast()
|
||||||
|
{
|
||||||
|
$podcast_model = new PodcastModel();
|
||||||
|
|
||||||
|
return $podcast_model->find($this->attributes['podcast_id']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,27 +7,57 @@
|
|||||||
|
|
||||||
namespace App\Entities;
|
namespace App\Entities;
|
||||||
|
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class Podcast extends Entity
|
class Podcast extends Entity
|
||||||
{
|
{
|
||||||
|
protected $link;
|
||||||
|
protected $image_url;
|
||||||
|
protected $episodes;
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'id' => 'integer',
|
'id' => 'integer',
|
||||||
'title' => 'string',
|
'title' => 'string',
|
||||||
'name' => 'string',
|
'name' => 'string',
|
||||||
'description' => 'string',
|
'description' => 'string',
|
||||||
'image' => 'string',
|
'image_uri' => 'string',
|
||||||
'language' => 'string',
|
'language' => 'string',
|
||||||
'category' => 'string',
|
'category' => 'string',
|
||||||
'explicit' => 'boolean',
|
'explicit' => 'boolean',
|
||||||
'author' => '?string',
|
'author_name' => '?string',
|
||||||
|
'author_email' => '?string',
|
||||||
'owner_name' => '?string',
|
'owner_name' => '?string',
|
||||||
'owner_email' => '?string',
|
'owner_email' => '?string',
|
||||||
'type' => '?string',
|
'type' => 'string',
|
||||||
'copyright' => '?string',
|
'copyright' => '?string',
|
||||||
'block' => 'boolean',
|
'block' => 'boolean',
|
||||||
'complete' => 'boolean',
|
'complete' => 'boolean',
|
||||||
'episode_description_footer' => '?string',
|
'episode_description_footer' => '?string',
|
||||||
'custom_html_head' => '?string',
|
'custom_html_head' => '?string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getImageUrl()
|
||||||
|
{
|
||||||
|
return media_url($this->attributes['image_uri']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLink()
|
||||||
|
{
|
||||||
|
return base_url(route_to('podcast_view', $this->attributes['name']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFeedUrl()
|
||||||
|
{
|
||||||
|
return base_url(route_to('podcast_feed', $this->attributes['name']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodes()
|
||||||
|
{
|
||||||
|
$episode_model = new EpisodeModel();
|
||||||
|
|
||||||
|
return $episode_model
|
||||||
|
->where('podcast_id', $this->attributes['id'])
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @copyright 2020 Podlibre
|
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
|
||||||
* @link https://castopod.org/
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all possible enum values for a table field
|
|
||||||
*
|
|
||||||
* @param string $table
|
|
||||||
* @param string $field
|
|
||||||
*
|
|
||||||
* @return array $enums
|
|
||||||
*/
|
|
||||||
function field_enums($table = '', $field = '')
|
|
||||||
{
|
|
||||||
$enums = [];
|
|
||||||
if ($table == '' || $field == '') {
|
|
||||||
return $enums;
|
|
||||||
}
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
preg_match_all(
|
|
||||||
"/'(.*?)'/",
|
|
||||||
$db->query("SHOW COLUMNS FROM {$table} LIKE '{$field}'")->getRow()
|
|
||||||
->Type,
|
|
||||||
$matches
|
|
||||||
);
|
|
||||||
foreach ($matches[1] as $value) {
|
|
||||||
$enums[$value] = $value;
|
|
||||||
}
|
|
||||||
return $enums;
|
|
||||||
}
|
|
@ -44,20 +44,20 @@ function write_file_tags($podcast, $episode)
|
|||||||
|
|
||||||
// Initialize getID3 tag-writing module
|
// Initialize getID3 tag-writing module
|
||||||
$tagwriter = new WriteTags();
|
$tagwriter = new WriteTags();
|
||||||
$tagwriter->filename = media_path($episode->enclosure_url);
|
$tagwriter->filename = $episode->enclosure_media_path;
|
||||||
|
|
||||||
// set various options (optional)
|
// set various options (optional)
|
||||||
$tagwriter->tagformats = ['id3v2.4'];
|
$tagwriter->tagformats = ['id3v2.4'];
|
||||||
$tagwriter->tag_encoding = $TextEncoding;
|
$tagwriter->tag_encoding = $TextEncoding;
|
||||||
|
|
||||||
$cover = new \CodeIgniter\Files\File(media_path($episode->image));
|
$cover = new \CodeIgniter\Files\File($episode->image_media_path);
|
||||||
|
|
||||||
$APICdata = file_get_contents($cover->getRealPath());
|
$APICdata = file_get_contents($cover->getRealPath());
|
||||||
|
|
||||||
// TODO: variables used for podcast specific tags
|
// TODO: variables used for podcast specific tags
|
||||||
// $podcast_url = base_url('@' . $podcast->name);
|
// $podcast_url = $podcast->link;
|
||||||
// $podcast_feed_url = base_url('@' . $podcast->name . '/feed.xml');
|
// $podcast_feed_url = $podcast->feed_url;
|
||||||
// $episode_media_url = media_url($podcast->name . '/' . $episode->slug);
|
// $episode_media_url = $episode->link;
|
||||||
|
|
||||||
// populate data array
|
// populate data array
|
||||||
$TagData = [
|
$TagData = [
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
* @param string $podcast_name
|
* @param string $podcast_name
|
||||||
* @param string $file_name
|
* @param string $file_name
|
||||||
*
|
*
|
||||||
* @return string The absolute path of the file in media root
|
* @return string The episode's file path in media root
|
||||||
*/
|
*/
|
||||||
function save_podcast_media($file, $podcast_name, $media_name)
|
function save_podcast_media($file, $podcast_name, $media_name)
|
||||||
{
|
{
|
||||||
|
165
app/Helpers/rss_helper.php
Normal file
165
app/Helpers/rss_helper.php
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Models\CategoryModel;
|
||||||
|
use CodeIgniter\I18n\Time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the rss feed for a given podcast entity
|
||||||
|
*
|
||||||
|
* @param App\Entities\Podcast $podcast
|
||||||
|
* @return string rss feed as xml
|
||||||
|
*/
|
||||||
|
function get_rss_feed($podcast)
|
||||||
|
{
|
||||||
|
$category_model = new CategoryModel();
|
||||||
|
|
||||||
|
$episodes = $podcast->episodes;
|
||||||
|
|
||||||
|
$podcast_category = $category_model
|
||||||
|
->where('code', $podcast->category)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||||
|
|
||||||
|
$rss = new SimpleRSSElement(
|
||||||
|
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='$itunes_namespace' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>"
|
||||||
|
);
|
||||||
|
|
||||||
|
$channel = $rss->addChild('channel');
|
||||||
|
|
||||||
|
$atom_link = $channel->addChild(
|
||||||
|
'atom:link',
|
||||||
|
null,
|
||||||
|
'http://www.w3.org/2005/Atom'
|
||||||
|
);
|
||||||
|
$atom_link->addAttribute('href', $podcast->feed_url);
|
||||||
|
$atom_link->addAttribute('rel', 'self');
|
||||||
|
$atom_link->addAttribute('type', 'application/rss+xml');
|
||||||
|
|
||||||
|
// the last build date corresponds to the creation of the feed.xml cache
|
||||||
|
$channel->addChild(
|
||||||
|
'lastBuildDate',
|
||||||
|
(new Time('now'))->format(DATE_RFC1123)
|
||||||
|
);
|
||||||
|
$channel->addChild(
|
||||||
|
'generator',
|
||||||
|
'Castopod 0.0.0-development - https://castopod.org'
|
||||||
|
);
|
||||||
|
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
|
||||||
|
|
||||||
|
$channel->addChild('title', $podcast->title);
|
||||||
|
$channel->addChildWithCDATA('description', $podcast->description);
|
||||||
|
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
|
||||||
|
$itunes_image->addAttribute('href', $podcast->image_url);
|
||||||
|
$channel->addChild('language', $podcast->language);
|
||||||
|
|
||||||
|
$itunes_category = $channel->addChild('category', null, $itunes_namespace);
|
||||||
|
$itunes_category->addAttribute(
|
||||||
|
'text',
|
||||||
|
$podcast_category->parent
|
||||||
|
? $podcast_category->parent->apple_category
|
||||||
|
: $podcast_category->apple_category
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($podcast_category->parent) {
|
||||||
|
$itunes_category_child = $itunes_category->addChild(
|
||||||
|
'category',
|
||||||
|
null,
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
$itunes_category_child->addAttribute(
|
||||||
|
'text',
|
||||||
|
$podcast_category->apple_category
|
||||||
|
);
|
||||||
|
$channel->addChild(
|
||||||
|
'category',
|
||||||
|
$podcast_category->parent->apple_category
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$channel->addChild('category', $podcast_category->apple_category);
|
||||||
|
|
||||||
|
$channel->addChild(
|
||||||
|
'explicit',
|
||||||
|
$podcast->explicit ? 'true' : 'false',
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
$podcast->author_name &&
|
||||||
|
$channel->addChild('author', $podcast->author_name, $itunes_namespace);
|
||||||
|
$channel->addChild('link', $podcast->link);
|
||||||
|
|
||||||
|
if ($podcast->owner_name || $podcast->owner_email) {
|
||||||
|
$owner = $channel->addChild('owner', null, $itunes_namespace);
|
||||||
|
$podcast->owner_name &&
|
||||||
|
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
|
||||||
|
$podcast->owner_email &&
|
||||||
|
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel->addChild('type', $podcast->type, $itunes_namespace);
|
||||||
|
$podcast->copyright && $channel->addChild('copyright', $podcast->copyright);
|
||||||
|
$podcast->block && $channel->addChild('block', 'Yes', $itunes_namespace);
|
||||||
|
$podcast->complete &&
|
||||||
|
$channel->addChild('complete', 'Yes', $itunes_namespace);
|
||||||
|
|
||||||
|
$image = $channel->addChild('image');
|
||||||
|
$image->addChild('url', $podcast->image_url);
|
||||||
|
$image->addChild('title', $podcast->title);
|
||||||
|
$image->addChild('link', $podcast->link);
|
||||||
|
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$item = $channel->addChild('item');
|
||||||
|
$item->addChild('title', $episode->title);
|
||||||
|
$enclosure = $item->addChild('enclosure');
|
||||||
|
$enclosure->addAttribute('url', $episode->enclosure_url);
|
||||||
|
$enclosure->addAttribute('length', $episode->enclosure_length);
|
||||||
|
$enclosure->addAttribute('type', $episode->enclosure_type);
|
||||||
|
|
||||||
|
$item->addChild('guid', $episode->guid);
|
||||||
|
$item->addChild('pubDate', $episode->pub_date->format(DATE_RFC1123));
|
||||||
|
$item->addChildWithCDATA('description', $episode->description);
|
||||||
|
$item->addChild('duration', $episode->duration, $itunes_namespace);
|
||||||
|
$item->addChild('link', $episode->link);
|
||||||
|
$episode_itunes_image = $item->addChild(
|
||||||
|
'image',
|
||||||
|
null,
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
$episode_itunes_image->addAttribute('href', $episode->image_url);
|
||||||
|
$item->addChild(
|
||||||
|
'explicit',
|
||||||
|
$episode->explicit ? 'true' : 'false',
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($episode->author_email || $episode->author_name) {
|
||||||
|
$item->addChild(
|
||||||
|
'author',
|
||||||
|
$episode->author_name
|
||||||
|
? $episode->author_email .
|
||||||
|
' (' .
|
||||||
|
$episode->author_name .
|
||||||
|
')'
|
||||||
|
: $episode->author_email
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->addChild('episode', $episode->number, $itunes_namespace);
|
||||||
|
$episode->season_number &&
|
||||||
|
$item->addChild(
|
||||||
|
'season',
|
||||||
|
$episode->season_number,
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
$item->addChild('episodeType', $episode->type, $itunes_namespace);
|
||||||
|
|
||||||
|
$episode->block && $item->addChild('block', 'Yes', $itunes_namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rss->asXML();
|
||||||
|
}
|
@ -16,23 +16,3 @@ function media_url($uri = '', string $protocol = null): string
|
|||||||
{
|
{
|
||||||
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
|
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the podcast URL to use in views
|
|
||||||
*
|
|
||||||
* @param mixed $uri URI string or array of URI segments
|
|
||||||
* @param string $protocol
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function podcast_url(
|
|
||||||
$podcast_id = 1,
|
|
||||||
$episode_id = 1,
|
|
||||||
$podcast_name = '',
|
|
||||||
$uri = '',
|
|
||||||
string $protocol = null
|
|
||||||
): string {
|
|
||||||
return base_url(
|
|
||||||
"/stats/$podcast_id/$episode_id/$podcast_name/$uri",
|
|
||||||
$protocol
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?
|
<?
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'back_to_podcast' => 'Go back to podcast',
|
||||||
'create' => 'Add an episode',
|
'create' => 'Add an episode',
|
||||||
'form' => [
|
'form' => [
|
||||||
'file' => 'Audio file',
|
'file' => 'Audio file',
|
||||||
@ -10,6 +11,8 @@ return [
|
|||||||
'pub_date' => 'Publication date',
|
'pub_date' => 'Publication date',
|
||||||
'image' => 'Image',
|
'image' => 'Image',
|
||||||
'explicit' => 'Explicit',
|
'explicit' => 'Explicit',
|
||||||
|
'author_name' => 'Author name',
|
||||||
|
'author_email' => 'Author email',
|
||||||
'type' => [
|
'type' => [
|
||||||
'label' => 'Type',
|
'label' => 'Type',
|
||||||
'full' => 'Full',
|
'full' => 'Full',
|
@ -11,7 +11,8 @@ return [
|
|||||||
'language' => 'Language',
|
'language' => 'Language',
|
||||||
'category' => 'Category',
|
'category' => 'Category',
|
||||||
'explicit' => 'Explicit',
|
'explicit' => 'Explicit',
|
||||||
'author' => 'Author',
|
'author_name' => 'Author name',
|
||||||
|
'author_email' => 'Author email',
|
||||||
'owner_name' => 'Owner name',
|
'owner_name' => 'Owner name',
|
||||||
'owner_email' => 'Owner email',
|
'owner_email' => 'Owner email',
|
||||||
'type' => [
|
'type' => [
|
||||||
@ -64,7 +65,7 @@ return [
|
|||||||
'courses' => 'Courses',
|
'courses' => 'Courses',
|
||||||
'how_to' => 'How To',
|
'how_to' => 'How To',
|
||||||
'language_learning' => 'Language Learning',
|
'language_learning' => 'Language Learning',
|
||||||
'self-improvement' => 'Self-Improvement',
|
'self_improvement' => 'Self-Improvement',
|
||||||
'comedy_fiction' => 'Comedy Fiction',
|
'comedy_fiction' => 'Comedy Fiction',
|
||||||
'drama' => 'Drama',
|
'drama' => 'Drama',
|
||||||
'science_fiction' => 'Science Fiction',
|
'science_fiction' => 'Science Fiction',
|
27
app/Libraries/SimpleRSSElement.php
Normal file
27
app/Libraries/SimpleRSSElement.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SimpleRSSElement extends SimpleXMLElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds a child with $value inside CDATA
|
||||||
|
* @param unknown $name
|
||||||
|
* @param unknown $value
|
||||||
|
*/
|
||||||
|
public function addChildWithCDATA($name, $value = null, $namespace = null)
|
||||||
|
{
|
||||||
|
$new_child = $this->addChild($name, null, $namespace);
|
||||||
|
|
||||||
|
if ($new_child !== null) {
|
||||||
|
$node = dom_import_simplexml($new_child);
|
||||||
|
$no = $node->ownerDocument;
|
||||||
|
$node->appendChild($no->createCDATASection($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_child;
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,12 @@ class CategoryModel extends Model
|
|||||||
protected $table = 'categories';
|
protected $table = 'categories';
|
||||||
protected $primaryKey = 'id';
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
protected $allowedFields = ['apple_category', 'google_category'];
|
protected $allowedFields = [
|
||||||
|
'parent_id',
|
||||||
|
'code',
|
||||||
|
'apple_category',
|
||||||
|
'google_category',
|
||||||
|
];
|
||||||
|
|
||||||
protected $returnType = 'App\Entities\Category';
|
protected $returnType = 'App\Entities\Category';
|
||||||
protected $useSoftDeletes = false;
|
protected $useSoftDeletes = false;
|
||||||
|
@ -18,17 +18,18 @@ class EpisodeModel extends Model
|
|||||||
'podcast_id',
|
'podcast_id',
|
||||||
'title',
|
'title',
|
||||||
'slug',
|
'slug',
|
||||||
'enclosure_url',
|
'enclosure_uri',
|
||||||
'enclosure_length',
|
'enclosure_length',
|
||||||
'enclosure_type',
|
'enclosure_type',
|
||||||
'guid',
|
|
||||||
'pub_date',
|
'pub_date',
|
||||||
'description',
|
'description',
|
||||||
'duration',
|
'duration',
|
||||||
'image',
|
'image_uri',
|
||||||
'explicit',
|
'explicit',
|
||||||
'number',
|
'number',
|
||||||
'season_number',
|
'season_number',
|
||||||
|
'author_name',
|
||||||
|
'author_email',
|
||||||
'type',
|
'type',
|
||||||
'block',
|
'block',
|
||||||
];
|
];
|
||||||
|
@ -20,11 +20,12 @@ class PodcastModel extends Model
|
|||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'episode_description_footer',
|
'episode_description_footer',
|
||||||
'image',
|
'image_uri',
|
||||||
'language',
|
'language',
|
||||||
'category',
|
'category',
|
||||||
'explicit',
|
'explicit',
|
||||||
'author',
|
'author_name',
|
||||||
|
'author_email',
|
||||||
'owner_name',
|
'owner_name',
|
||||||
'owner_email',
|
'owner_email',
|
||||||
'type',
|
'type',
|
||||||
|
105
app/Views/episode/create.php
Normal file
105
app/Views/episode/create.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?= $this->extend('layouts/default') ?>
|
||||||
|
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
|
<h1 class="mb-6 text-xl"><?= lang('Episode.create') ?></h1>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<?= \Config\Services::validation()->listErrors() ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?= form_open_multipart(route_to('episode_create', $podcast->name), [
|
||||||
|
'method' => 'post',
|
||||||
|
'class' => 'flex flex-col max-w-md',
|
||||||
|
]) ?>
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="episode_file"><?= lang('Episode.form.file') ?></label>
|
||||||
|
<input type="file" class="form-input" id="episode_file" name="episode_file" required accept=".mp3,.m4a" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="title"><?= lang('Episode.form.title') ?></label>
|
||||||
|
<input type="text" class="form-input" id="title" name="title" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="slug"><?= lang('Episode.form.slug') ?></label>
|
||||||
|
<input type="text" class="form-input" id="slug" name="slug" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="description"><?= lang('Episode.form.description') ?></label>
|
||||||
|
<textarea class="form-textarea" id="description" name="description" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="pub_date"><?= lang('Episode.form.pub_date') ?></label>
|
||||||
|
<input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date(
|
||||||
|
'Y-m-d'
|
||||||
|
) ?>" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="image"><?= lang('Episode.form.image') ?></label>
|
||||||
|
<input type="file" class="form-input" id="image" name="image" accept=".jpg,.jpeg,.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="episode_number"><?= lang(
|
||||||
|
'Episode.form.episode_number'
|
||||||
|
) ?></label>
|
||||||
|
<input type="number" class="form-input" id="episode_number" name="episode_number" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="season_number"><?= lang('Episode.form.season_number') ?></label>
|
||||||
|
<input type="number" class="form-input" id="season_number" name="season_number" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-flex items-center mb-4">
|
||||||
|
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
|
||||||
|
<label for="explicit" class="pl-2"><?= lang(
|
||||||
|
'Episode.form.explicit'
|
||||||
|
) ?></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
|
||||||
|
<input type="text" class="form-input" id="author_name" name="author_name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
|
||||||
|
<input type="email" class="form-input" id="author_email" name="author_email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="flex flex-col mb-4">
|
||||||
|
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||||
|
<label for="full" class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" value="full" id="full" name="type" required checked />
|
||||||
|
<span class="ml-2"><?= lang('Episode.form.type.full') ?></span>
|
||||||
|
</label>
|
||||||
|
<label for="trailer" class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required />
|
||||||
|
<span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>
|
||||||
|
</label>
|
||||||
|
<label for="bonus" class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required />
|
||||||
|
<span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="inline-flex items-center mb-4">
|
||||||
|
<input type="checkbox" id="block" name="block" class="form-checkbox" />
|
||||||
|
<label for="block" class="pl-2"><?= lang('Episode.form.block') ?></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
||||||
|
'Episode.form.submit'
|
||||||
|
) ?></button>
|
||||||
|
<?= form_close() ?>
|
||||||
|
|
||||||
|
|
||||||
|
<?= $this->endSection() ?>
|
16
app/Views/episode/view.php
Normal file
16
app/Views/episode/view.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?= $this->extend('layouts/default') ?>
|
||||||
|
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
|
<a class="underline hover:no-underline" href="<?= route_to(
|
||||||
|
'podcast_view',
|
||||||
|
$podcast->name
|
||||||
|
) ?>">< <?= lang('Episode.back_to_podcast') ?></a>
|
||||||
|
<h1 class="text-2xl font-semibold"><?= $episode->title ?></h1>
|
||||||
|
<img src="<?= $episode->image_url ?>" alt="Episode cover" class="object-cover w-40 h-40 mb-6" />
|
||||||
|
<audio controls preload="none">
|
||||||
|
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||||
|
Your browser does not support the audio tag.
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<?= $this->endSection() ?>
|
@ -1,91 +0,0 @@
|
|||||||
<?= $this->extend('layouts/default') ?>
|
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
|
||||||
|
|
||||||
<h1 class="mb-6 text-xl"><?= lang('Episodes.create') ?></h1>
|
|
||||||
|
|
||||||
<div class="mb-8">
|
|
||||||
<?= \Config\Services::validation()->listErrors() ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?= form_open_multipart(route_to('episodes_create', '@' . $podcast->name), [
|
|
||||||
'method' => 'post',
|
|
||||||
'class' => 'flex flex-col max-w-md',
|
|
||||||
]) ?>
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="episode_file"><?= lang('Episodes.form.file') ?></label>
|
|
||||||
<input type="file" class="form-input" id="episode_file" name="episode_file" required accept=".mp3,.m4a" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="title"><?= lang('Episodes.form.title') ?></label>
|
|
||||||
<input type="text" class="form-input" id="title" name="title" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="slug"><?= lang('Episodes.form.slug') ?></label>
|
|
||||||
<input type="text" class="form-input" id="slug" name="slug" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="description"><?= lang('Episodes.form.description') ?></label>
|
|
||||||
<textarea class="form-textarea" id="description" name="description" required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="pub_date"><?= lang('Episodes.form.pub_date') ?></label>
|
|
||||||
<input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date(
|
|
||||||
'Y-m-d'
|
|
||||||
) ?>" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="image"><?= lang('Episodes.form.image') ?></label>
|
|
||||||
<input type="file" class="form-input" id="image" name="image" accept=".jpg,.jpeg,.png" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="episode_number"><?= lang(
|
|
||||||
'Episodes.form.episode_number'
|
|
||||||
) ?></label>
|
|
||||||
<input type="number" class="form-input" id="episode_number" name="episode_number" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
|
||||||
<label for="season_number"><?= lang(
|
|
||||||
'Episodes.form.season_number'
|
|
||||||
) ?></label>
|
|
||||||
<input type="number" class="form-input" id="season_number" name="season_number" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inline-flex items-center mb-4">
|
|
||||||
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
|
|
||||||
<label for="explicit" class="pl-2"><?= lang(
|
|
||||||
'Episodes.form.explicit'
|
|
||||||
) ?></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="flex flex-col mb-4">
|
|
||||||
<legend><?= lang('Episodes.form.type.label') ?></legend>
|
|
||||||
<?php foreach ($episode_types as $type): ?>
|
|
||||||
<label for="<?= $type ?>" class="inline-flex items-center">
|
|
||||||
<input type="radio" class="form-radio" value="<?= $type ?>" id="<?= $type ?>" name="type" required />
|
|
||||||
<span class="ml-2"><?= lang('Episodes.form.type.' . $type) ?></span>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="inline-flex items-center mb-4">
|
|
||||||
<input type="checkbox" id="block" name="block" class="form-checkbox" />
|
|
||||||
<label for="block" class="pl-2"><?= lang('Episodes.form.block') ?></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
|
||||||
'Episodes.form.submit'
|
|
||||||
) ?></button>
|
|
||||||
<?= form_close() ?>
|
|
||||||
|
|
||||||
|
|
||||||
<?= $this->endSection() ?>
|
|
@ -8,13 +8,9 @@
|
|||||||
<section class="flex flex-wrap">
|
<section class="flex flex-wrap">
|
||||||
<?php if ($podcasts): ?>
|
<?php if ($podcasts): ?>
|
||||||
<?php foreach ($podcasts as $podcast): ?>
|
<?php foreach ($podcasts as $podcast): ?>
|
||||||
<a href="<?= route_to('podcasts_view', '@' . $podcast->name) ?>">
|
<a href="<?= route_to('podcast_view', $podcast->name) ?>">
|
||||||
<article class="w-48 p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
|
<article class="w-48 p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
|
||||||
<img alt="<?= $podcast->title ?>"
|
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
|
||||||
src="<?= media_url(
|
|
||||||
$podcast->image
|
|
||||||
) ?>" class="object-cover w-full h-40 mb-2"
|
|
||||||
/>
|
|
||||||
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
|
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
|
||||||
<p class="text-gray-600">@<?= $podcast->name ?></p>
|
<p class="text-gray-600">@<?= $podcast->name ?></p>
|
||||||
</article>
|
</article>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<a href="<?= route_to('home') ?>" class="text-2xl">Castopod</a>
|
<a href="<?= route_to('home') ?>" class="text-2xl">Castopod</a>
|
||||||
<nav>
|
<nav>
|
||||||
<a class="px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
<a class="px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
||||||
'podcasts_create'
|
'podcast_create'
|
||||||
) ?>">New podcast</a>
|
) ?>">New podcast</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,63 +2,66 @@
|
|||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
<h1 class="mb-6 text-xl"><?= lang('Podcasts.create') ?></h1>
|
<h1 class="mb-6 text-xl"><?= lang('Podcast.create') ?></h1>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<?= \Config\Services::validation()->listErrors() ?>
|
<?= \Config\Services::validation()->listErrors() ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?= form_open_multipart(base_url(route_to('podcasts_create')), [
|
<?= form_open_multipart(base_url(route_to('podcast_create')), [
|
||||||
'method' => 'post',
|
'method' => 'post',
|
||||||
'class' => 'flex flex-col max-w-md',
|
'class' => 'flex flex-col max-w-md',
|
||||||
]) ?>
|
]) ?>
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="title"><?= lang('Podcasts.form.title') ?></label>
|
<label for="title"><?= lang('Podcast.form.title') ?></label>
|
||||||
<input type="text" class="form-input" id="title" name="title" required />
|
<input type="text" class="form-input" id="title" name="title" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="name"><?= lang('Podcasts.form.name') ?></label>
|
<label for="name"><?= lang('Podcast.form.name') ?></label>
|
||||||
<input type="text" class="form-input" id="name" name="name" required />
|
<input type="text" class="form-input" id="name" name="name" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="description"><?= lang('Podcasts.form.description') ?></label>
|
<label for="description"><?= lang('Podcast.form.description') ?></label>
|
||||||
<textarea class="form-textarea" id="description" name="description" required></textarea>
|
<textarea class="form-textarea" id="description" name="description" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="episode_description_footer"><?= lang(
|
<label for="episode_description_footer"><?= lang(
|
||||||
'Podcasts.form.episode_description_footer'
|
'Podcast.form.episode_description_footer'
|
||||||
) ?></label>
|
) ?></label>
|
||||||
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"></textarea>
|
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="image"><?= lang('Podcasts.form.image') ?></label>
|
<label for="image"><?= lang('Podcast.form.image') ?></label>
|
||||||
<input type="file" class="form-input" id="image" name="image" required />
|
<input type="file" class="form-input" id="image" name="image" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="language"><?= lang('Podcasts.form.language') ?></label>
|
<label for="language"><?= lang('Podcast.form.language') ?></label>
|
||||||
<select id="language" name="language" autocomplete="off" class="form-select" required>
|
<select id="language" name="language" autocomplete="off" class="form-select" required>
|
||||||
<?php foreach ($languages as $language): ?>
|
<?php foreach ($languages as $language): ?>
|
||||||
<option <?= $language->code == $browser_lang
|
<option <?= $language->code == $browser_lang
|
||||||
? "selected='selected'"
|
? "selected='selected'"
|
||||||
: '' ?> value="<?= $language->code ?>"><?= $language->native_name ?></option>
|
: '' ?> value="<?= $language->code ?>">
|
||||||
|
<?= $language->native_name ?>
|
||||||
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="category"><?= lang('Podcasts.form.category') ?></label>
|
<label for="category"><?= lang('Podcast.form.category') ?></label>
|
||||||
<select id="category" name="category" class="form-select" required>
|
<select id="category" name="category" class="form-select" required>
|
||||||
<?php foreach ($categories as $category): ?>
|
<?php foreach ($categories as $category): ?>
|
||||||
<option value="<?= $category->code ?>"><?= lang(
|
<option value="<?= $category->code ?>"><?= lang(
|
||||||
'Podcasts.category_options.' . $category->code
|
'Podcast.category_options.' . $category->code
|
||||||
) ?></option>
|
) ?>
|
||||||
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -66,61 +69,68 @@
|
|||||||
<div class="inline-flex items-center mb-4">
|
<div class="inline-flex items-center mb-4">
|
||||||
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
|
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
|
||||||
<label for="explicit" class="pl-2"><?= lang(
|
<label for="explicit" class="pl-2"><?= lang(
|
||||||
'Podcasts.form.explicit'
|
'Podcast.form.explicit'
|
||||||
) ?></label>
|
) ?></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="author"><?= lang('Podcasts.form.author') ?></label>
|
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
|
||||||
<input type="text" class="form-input" id="author" name="author" />
|
<input type="text" class="form-input" id="author_name" name="author_name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="owner_name"><?= lang('Podcasts.form.owner_name') ?></label>
|
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
|
||||||
|
<input type="email" class="form-input" id="author_email" name="author_email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col mb-4">
|
||||||
|
<label for="owner_name"><?= lang('Podcast.form.owner_name') ?></label>
|
||||||
<input type="text" class="form-input" id="owner_name" name="owner_name" />
|
<input type="text" class="form-input" id="owner_name" name="owner_name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="owner_email"><?= lang('Podcasts.form.owner_email') ?></label>
|
<label for="owner_email"><?= lang('Podcast.form.owner_email') ?></label>
|
||||||
<input type="email" class="form-input" id="owner_email" name="owner_email" required />
|
<input type="email" class="form-input" id="owner_email" name="owner_email" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="flex flex-col mb-4">
|
<fieldset class="flex flex-col mb-4">
|
||||||
<legend><?= lang('Podcasts.form.type.label') ?></legend>
|
<legend><?= lang('Podcast.form.type.label') ?></legend>
|
||||||
<?php foreach ($podcast_types as $type): ?>
|
<label for="episodic" class="inline-flex items-center">
|
||||||
<label for="<?= $type ?>" class="inline-flex items-center">
|
<input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required checked />
|
||||||
<input type="radio" class="form-radio" value="<?= $type ?>" id="<?= $type ?>" name="type" required />
|
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
|
||||||
<span class="ml-2"><?= lang('Podcasts.form.type.' . $type) ?></span>
|
</label>
|
||||||
|
<label for="serial" class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" value="serial" id="serial" name="type" required />
|
||||||
|
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
|
||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="copyright"><?= lang('Podcasts.form.copyright') ?></label>
|
<label for="copyright"><?= lang('Podcast.form.copyright') ?></label>
|
||||||
<input type="text" class="form-input" id="copyright" name="copyright" />
|
<input type="text" class="form-input" id="copyright" name="copyright" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inline-flex items-center mb-4">
|
<div class="inline-flex items-center mb-4">
|
||||||
<input type="checkbox" id="block" name="block" class="form-checkbox" />
|
<input type="checkbox" id="block" name="block" class="form-checkbox" />
|
||||||
<label for="block" class="pl-2"><?= lang('Podcasts.form.block') ?></label>
|
<label for="block" class="pl-2"><?= lang('Podcast.form.block') ?></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inline-flex items-center mb-4">
|
<div class="inline-flex items-center mb-4">
|
||||||
<input type="checkbox" id="complete" name="complete" class="form-checkbox" />
|
<input type="checkbox" id="complete" name="complete" class="form-checkbox" />
|
||||||
<label for="complete" class="pl-2"><?= lang(
|
<label for="complete" class="pl-2"><?= lang(
|
||||||
'Podcasts.form.complete'
|
'Podcast.form.complete'
|
||||||
) ?></label>
|
) ?></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col mb-4">
|
||||||
<label for="custom_html_head"><?= esc(
|
<label for="custom_html_head"><?= esc(
|
||||||
lang('Podcasts.form.custom_html_head')
|
lang('Podcast.form.custom_html_head')
|
||||||
) ?></label>
|
) ?></label>
|
||||||
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head"></textarea>
|
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
||||||
'Podcasts.form.submit'
|
'Podcast.form.submit'
|
||||||
) ?></button>
|
) ?></button>
|
||||||
<?= form_close() ?>
|
<?= form_close() ?>
|
||||||
|
|
@ -3,29 +3,29 @@
|
|||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<header class="py-4 border-b">
|
<header class="py-4 border-b">
|
||||||
<h1 class="text-2xl"><?= $podcast->title ?></h1>
|
<h1 class="text-2xl"><?= $podcast->title ?></h1>
|
||||||
<img src="<?= media_url(
|
<img src="<?= $podcast->image_url ?>" alt="Podcast cover" class="w-40 h-40 mb-6" />
|
||||||
$podcast->image
|
|
||||||
) ?>" alt="Podcast cover" class="w-40 h-40 mb-6" />
|
|
||||||
<a class="inline-flex px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
<a class="inline-flex px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
||||||
'episodes_create',
|
'episode_create',
|
||||||
'@' . $podcast->name
|
$podcast->name
|
||||||
) ?>">New Episode</a>
|
) ?>">New Episode</a>
|
||||||
|
<a class="inline-flex px-4 py-2 bg-orange-500 hover:bg-orange-600" href="<?= route_to(
|
||||||
|
'podcast_feed',
|
||||||
|
$podcast->name
|
||||||
|
) ?>">RSS feed</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="flex flex-col py-4">
|
<section class="flex flex-col py-4">
|
||||||
<h2 class="mb-4 text-xl"><?= lang(
|
<h2 class="mb-4 text-xl"><?= lang('Podcast.list_of_episodes') ?> (<?= count(
|
||||||
'Podcasts.list_of_episodes'
|
$episodes
|
||||||
) ?> (<?= count($episodes) ?>)</h2>
|
) ?>)</h2>
|
||||||
<?php if ($episodes): ?>
|
<?php if ($episodes): ?>
|
||||||
<?php foreach ($episodes as $episode): ?>
|
<?php foreach ($episodes as $episode): ?>
|
||||||
<article class="flex w-full max-w-lg p-4 mb-4 border shadow">
|
<article class="flex w-full max-w-lg p-4 mb-4 border shadow">
|
||||||
<img src="<?= media_url(
|
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
|
||||||
$episode->image ? $episode->image : $podcast->image
|
|
||||||
) ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
|
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'episodes_view',
|
'episode_view',
|
||||||
'@' . $podcast->name,
|
$podcast->name,
|
||||||
$episode->slug
|
$episode->slug
|
||||||
) ?>">
|
) ?>">
|
||||||
<h3 class="text-xl font-semibold">
|
<h3 class="text-xl font-semibold">
|
||||||
@ -35,19 +35,14 @@
|
|||||||
<p><?= $episode->description ?></p>
|
<p><?= $episode->description ?></p>
|
||||||
</a>
|
</a>
|
||||||
<audio controls class="mt-auto" preload="none">
|
<audio controls class="mt-auto" preload="none">
|
||||||
<source src="<?= podcast_url(
|
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||||
$episode->podcast_id,
|
|
||||||
$episode->id,
|
|
||||||
$podcast->name,
|
|
||||||
$episode->enclosure_url
|
|
||||||
) ?>" type="<?= $episode->enclosure_type ?>">
|
|
||||||
Your browser does not support the audio tag.
|
Your browser does not support the audio tag.
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="italic"><?= lang('Podcasts.no_episode') ?></p>
|
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
4234
composer.lock
generated
4234
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -117,7 +117,7 @@ You can install / update the project's dependencies using both `composer` and `n
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# install php dependencies
|
# install php dependencies
|
||||||
docker-compose run --rm composer update --ignore-platform-reqs
|
docker-compose run --rm composer install --ignore-platform-reqs
|
||||||
|
|
||||||
# update php dependencies
|
# update php dependencies
|
||||||
docker-compose run --rm composer update --ignore-platform-reqs
|
docker-compose run --rm composer update --ignore-platform-reqs
|
||||||
|
Loading…
Reference in New Issue
Block a user