From 9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d Mon Sep 17 00:00:00 2001 From: Benjamin Bellamy <ben@podlibre.org> Date: Fri, 21 Aug 2020 08:41:09 +0000 Subject: [PATCH] feat: import podcast from an rss feed url * add podcast import form * add League\\HTMLToMarkdown * add guid field in podcast table * change podcast category from string to id closes #21 --- DEPENDENCIES.md | 3 +- app/Config/Routes.php | 11 + app/Controllers/Admin/Podcast.php | 226 +++++++++++++++++- .../2020-05-30-101500_add_podcasts.php | 17 +- .../2020-06-05-170000_add_episodes.php | 11 +- .../2020-06-05-190000_add_platforms.php | 2 + app/Database/Seeds/AuthSeeder.php | 5 + app/Database/Seeds/PlatformSeeder.php | 13 + app/Entities/Episode.php | 45 ++-- app/Entities/Podcast.php | 11 +- app/Helpers/media_helper.php | 23 +- app/Helpers/misc_helper.php | 108 +++++++++ app/Helpers/rss_helper.php | 4 +- app/Language/en/Breadcrumb.php | 1 + app/Language/en/Podcast.php | 41 +++- app/Models/EpisodeModel.php | 5 +- app/Models/PodcastModel.php | 6 +- app/Views/admin/podcast/edit.php | 2 +- app/Views/admin/podcast/import.php | 163 +++++++++++++ app/Views/admin/podcast/list.php | 5 + composer.json | 3 +- composer.lock | 132 ++++++++-- docs/setup-development.md | 12 + 23 files changed, 780 insertions(+), 69 deletions(-) create mode 100644 app/Views/admin/podcast/import.php diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 612c3284a3..fed484c99b 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -5,13 +5,13 @@ Castopod uses the following components: PHP Dependencies: - [Code Igniter 4](https://codeigniter.com) ([MIT License](https://codeigniter.com/user_guide/license.html)) -- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE)) - [WhichBrowser/Parser-PHP](https://github.com/WhichBrowser/Parser-PHP) ([MIT License](https://github.com/WhichBrowser/Parser-PHP/blob/master/LICENSE)) - [GeoIP2 PHP API](https://github.com/maxmind/GeoIP2-php) ([Apache License 2.0](https://github.com/maxmind/GeoIP2-php/blob/master/LICENSE)) - [getID3](https://github.com/JamesHeinrich/getID3) ([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt)) - [myth-auth](https://github.com/lonnieezell/myth-auth) ([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md)) - [commonmark](https://commonmark.thephpleague.com/) ([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE)) - [phpdotenv](https://github.com/vlucas/phpdotenv) ([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE)) +- [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown) ([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE)) Javascript dependencies: @@ -24,3 +24,4 @@ Javascript dependencies: Other: - [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License)) +- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE)) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index adec234ceb..be223070b6 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -71,6 +71,10 @@ $routes->group( 'as' => 'admin', ]); + $routes->get('my-podcasts', 'Podcast::myPodcasts', [ + 'as' => 'my-podcasts', + ]); + // Podcasts $routes->group('podcasts', function ($routes) { $routes->get('/', 'Podcast::list', [ @@ -83,6 +87,13 @@ $routes->group( $routes->post('new', 'Podcast::attemptCreate', [ 'filter' => 'permission:podcasts-create', ]); + $routes->get('import', 'Podcast::import', [ + 'as' => 'podcast-import', + 'filter' => 'permission:podcasts-import', + ]); + $routes->post('import', 'Podcast::attemptImport', [ + 'filter' => 'permission:podcasts-import', + ]); // Podcast // Use ids in admin area to help permission and group lookups diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 6de23cf9c8..adfdeaac19 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -11,7 +11,9 @@ namespace App\Controllers\Admin; use App\Models\CategoryModel; use App\Models\LanguageModel; use App\Models\PodcastModel; +use App\Models\EpisodeModel; use Config\Services; +use League\HTMLToMarkdown\HtmlConverter; class Podcast extends BaseController { @@ -69,7 +71,7 @@ class Podcast extends BaseController $categoryOptions = array_reduce( $categories, function ($result, $category) { - $result[$category->code] = lang( + $result[$category->id] = lang( 'Podcast.category_options.' . $category->code ); return $result; @@ -110,7 +112,7 @@ class Podcast extends BaseController ), 'image' => $this->request->getFile('image'), 'language' => $this->request->getPost('language'), - 'category' => $this->request->getPost('category'), + 'category_id' => $this->request->getPost('category'), 'explicit' => $this->request->getPost('explicit') == 'yes', 'author' => $this->request->getPost('author'), 'owner_name' => $this->request->getPost('owner_name'), @@ -151,6 +153,222 @@ class Podcast extends BaseController return redirect()->route('podcast-view', [$newPodcastId]); } + public function import() + { + helper(['form', 'misc']); + + $categories = (new CategoryModel())->findAll(); + $languages = (new LanguageModel())->findAll(); + $languageOptions = array_reduce( + $languages, + function ($result, $language) { + $result[$language->code] = $language->native_name; + return $result; + }, + [] + ); + $categoryOptions = array_reduce( + $categories, + function ($result, $category) { + $result[$category->id] = lang( + 'Podcast.category_options.' . $category->code + ); + return $result; + }, + [] + ); + + $data = [ + 'languageOptions' => $languageOptions, + 'categoryOptions' => $categoryOptions, + 'browserLang' => get_browser_language( + $this->request->getServer('HTTP_ACCEPT_LANGUAGE') + ), + ]; + + return view('admin/podcast/import', $data); + } + + public function attemptImport() + { + helper(['media', 'misc']); + + $rules = [ + 'name' => 'required', + 'imported_feed_url' => 'required', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + try { + $feed = simplexml_load_file( + $this->request->getPost('imported_feed_url') + ); + } catch (\ErrorException $ex) { + return redirect() + ->back() + ->withInput() + ->with('errors', [ + $ex->getMessage() . + ': <a href="' . + $this->request->getPost('imported_feed_url') . + '" rel="noreferrer noopener" target="_blank">' . + $this->request->getPost('imported_feed_url') . + ' ⎋</a>', + ]); + } + + $nsItunes = $feed->channel[0]->children( + 'http://www.itunes.com/dtds/podcast-1.0.dtd' + ); + + $podcast = new \App\Entities\Podcast([ + 'name' => $this->request->getPost('name'), + 'imported_feed_url' => $this->request->getPost('imported_feed_url'), + + 'title' => $feed->channel[0]->title, + 'description' => $feed->channel[0]->description, + 'image' => download_file($nsItunes->image->attributes()), + 'language' => $this->request->getPost('language'), + 'category_id' => $this->request->getPost('category'), + 'explicit' => empty($nsItunes->explicit) + ? false + : $nsItunes->explicit == 'yes', + 'author' => $nsItunes->author, + 'owner_name' => $nsItunes->owner->name, + 'owner_email' => $nsItunes->owner->email, + 'type' => empty($nsItunes->type) ? 'episodic' : $nsItunes->type, + 'copyright' => $feed->channel[0]->copyright, + 'block' => empty($nsItunes->block) + ? false + : $nsItunes->block == 'yes', + 'complete' => empty($nsItunes->complete) + ? false + : $nsItunes->complete == 'yes', + 'episode_description_footer' => '', + 'custom_html_head' => '', + 'created_by' => user(), + 'updated_by' => user(), + ]); + + $podcastModel = new PodcastModel(); + $db = \Config\Database::connect(); + + $db->transStart(); + + if (!($newPodcastId = $podcastModel->insert($podcast, true))) { + $db->transComplete(); + return redirect() + ->back() + ->withInput() + ->with('errors', $podcastModel->errors()); + } + + $authorize = Services::authorization(); + $podcastAdminGroup = $authorize->group('podcast_admin'); + + $podcastModel->addPodcastContributor( + user()->id, + $newPodcastId, + $podcastAdminGroup->id + ); + + $converter = new HtmlConverter(); + + $numberItems = $feed->channel[0]->item->count(); + $lastItem = + !empty($this->request->getPost('max_episodes')) && + $this->request->getPost('max_episodes') < $numberItems + ? $this->request->getPost('max_episodes') + : $numberItems; + + $slugs = []; + + // For each Episode: + for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) { + $item = $feed->channel[0]->item[$numberItems - $itemNumber]; + + $nsItunes = $item->children( + 'http://www.itunes.com/dtds/podcast-1.0.dtd' + ); + + $slug = slugify( + $this->request->getPost('slug_field') == 'title' + ? $item->title + : basename($item->link) + ); + if (in_array($slug, $slugs)) { + $slugNumber = 2; + while (in_array($slug . '-' . $slugNumber, $slugs)) { + $slugNumber++; + } + $slug = $slug . '-' . $slugNumber; + } + $slugs[] = $slug; + + $newEpisode = new \App\Entities\Episode([ + 'podcast_id' => $newPodcastId, + 'guid' => empty($item->guid) ? null : $item->guid, + 'title' => $item->title, + 'slug' => $slug, + 'enclosure' => download_file($item->enclosure->attributes()), + 'description' => $converter->convert( + $this->request->getPost('description_field') == 'summary' + ? $nsItunes->summary + : ($this->request->getPost('description_field') == + 'subtitle_summary' + ? '<h3>' . + $nsItunes->subtitle . + "</h3>\n" . + $nsItunes->summary + : $item->description) + ), + 'image' => empty($nsItunes->image->attributes()) + ? null + : download_file($nsItunes->image->attributes()), + 'explicit' => $nsItunes->explicit == 'yes', + 'number' => $this->request->getPost('force_renumber') + ? $itemNumber + : $nsItunes->episode, + 'season_number' => empty( + $this->request->getPost('season_number') + ) + ? $nsItunes->season + : $this->request->getPost('season_number'), + 'type' => empty($nsItunes->episodeType) + ? 'full' + : $nsItunes->episodeType, + 'block' => empty($nsItunes->block) + ? false + : $nsItunes->block == 'yes', + 'created_by' => user(), + 'updated_by' => user(), + ]); + $newEpisode->setPublishedAt( + date('Y-m-d', strtotime($item->pubDate)), + date('H:i:s', strtotime($item->pubDate)) + ); + + $episodeModel = new EpisodeModel(); + + if (!$episodeModel->save($newEpisode)) { + // FIX: What shall we do? + return redirect() + ->back() + ->withInput() + ->with('errors', $episodeModel->errors()); + } + } + + $db->transComplete(); + + return redirect()->route('podcast-list'); + } + public function edit() { helper('form'); @@ -168,7 +386,7 @@ class Podcast extends BaseController $categoryOptions = array_reduce( $categories, function ($result, $category) { - $result[$category->code] = lang( + $result[$category->id] = lang( 'Podcast.category_options.' . $category->code ); return $result; @@ -212,7 +430,7 @@ class Podcast extends BaseController $this->podcast->image = $image; } $this->podcast->language = $this->request->getPost('language'); - $this->podcast->category = $this->request->getPost('category'); + $this->podcast->category_id = $this->request->getPost('category'); $this->podcast->explicit = $this->request->getPost('explicit') == 'yes'; $this->podcast->author = $this->request->getPost('author'); $this->podcast->owner_name = $this->request->getPost('owner_name'); diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php index 5aa7aa20c4..4c024439dc 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -44,10 +44,11 @@ class AddPodcasts extends Migration 'type' => 'VARCHAR', 'constraint' => 2, ], - 'category' => [ - 'type' => 'VARCHAR', - 'constraint' => 1024, - 'null' => true, + 'category_id' => [ + 'type' => 'INT', + 'constraint' => 10, + 'unsigned' => true, + 'default' => 0, ], 'explicit' => [ 'type' => 'TINYINT', @@ -105,6 +106,13 @@ class AddPodcasts extends Migration 'constraint' => 11, 'unsigned' => true, ], + 'imported_feed_url' => [ + 'type' => 'VARCHAR', + 'constraint' => 1024, + 'comment' => + 'The RSS feed URL if this podcast was imported, NULL otherwise.', + 'null' => true, + ], 'created_at' => [ 'type' => 'TIMESTAMP', ], @@ -117,6 +125,7 @@ class AddPodcasts extends Migration ], ]); $this->forge->addKey('id', true); + $this->forge->addForeignKey('category_id', 'categories', 'id'); $this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('updated_by', 'users', 'id'); $this->forge->createTable('podcasts'); diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index c0fa74af15..49625900ba 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -29,6 +29,10 @@ class AddEpisodes extends Migration 'constraint' => 20, 'unsigned' => true, ], + 'guid' => [ + 'type' => 'VARCHAR', + 'constraint' => 191, + ], 'title' => [ 'type' => 'VARCHAR', 'constraint' => 1024, @@ -59,16 +63,17 @@ class AddEpisodes extends Migration 'type' => 'INT', 'constraint' => 10, 'unsigned' => true, + 'null' => true, ], 'season_number' => [ 'type' => 'INT', 'constraint' => 10, 'unsigned' => true, - 'default' => 1, + 'null' => true, ], 'type' => [ 'type' => 'ENUM', - 'constraint' => ['full', 'trailer', 'bonus'], + 'constraint' => ['trailer', 'full', 'bonus'], 'default' => 'full', ], 'block' => [ @@ -103,8 +108,6 @@ class AddEpisodes extends Migration ]); $this->forge->addKey('id', true); $this->forge->addUniqueKey(['podcast_id', 'slug']); - - $this->forge->addUniqueKey(['podcast_id', 'season_number', 'number']); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); $this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('updated_by', 'users', 'id'); diff --git a/app/Database/Migrations/2020-06-05-190000_add_platforms.php b/app/Database/Migrations/2020-06-05-190000_add_platforms.php index c65929d098..c012efd6b1 100644 --- a/app/Database/Migrations/2020-06-05-190000_add_platforms.php +++ b/app/Database/Migrations/2020-06-05-190000_add_platforms.php @@ -66,6 +66,8 @@ class AddPlatforms extends Migration 'type' => 'TINYINT', 'constraint' => 1, 'default' => 0, + 'comment' => + 'Android deeplinking for this platform: 0=No, 1=Manual, 2=Automatic.', ], 'logo_file_name' => [ 'type' => 'VARCHAR', diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index 6ddfd95012..06286125e8 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -90,6 +90,11 @@ class AuthSeeder extends Seeder 'description' => 'Add a new podcast', 'has_permission' => ['superadmin'], ], + [ + 'name' => 'import', + 'description' => 'Import a new podcast from an external feed', + 'has_permission' => ['superadmin'], + ], [ 'name' => 'list', 'description' => 'List all podcasts and their episodes', diff --git a/app/Database/Seeds/PlatformSeeder.php b/app/Database/Seeds/PlatformSeeder.php index fd02b74396..c46a0e62f4 100644 --- a/app/Database/Seeds/PlatformSeeder.php +++ b/app/Database/Seeds/PlatformSeeder.php @@ -209,6 +209,19 @@ class PlatformSeeder extends Seeder 'android_deeplink' => 2, 'logo_file_name' => 'Podbean.png', ], + [ + 'name' => 'Podcast Addict', + 'home_url' => 'https://podcastaddict.com/', + 'submit_url' => 'https://podcastaddict.com/submit', + 'iosapp_url' => '', + 'androidapp_url' => + 'https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict', + 'comment' => '', + 'display_by_default' => 0, + 'ios_deeplink' => 0, + 'android_deeplink' => 2, + 'logo_file_name' => 'podcastaddict.svg', + ], [ 'name' => 'Podcastland', 'home_url' => 'https://podcastland.com/', diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 45f6bae675..1ddc42ee39 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -19,11 +19,6 @@ class Episode extends Entity */ protected $podcast; - /** - * @var string - */ - protected $GUID; - /** * @var string */ @@ -77,13 +72,14 @@ class Episode extends Entity ]; protected $casts = [ + 'guid' => 'string', 'slug' => 'string', 'title' => 'string', 'enclosure_uri' => 'string', 'description' => 'string', 'image_uri' => '?string', 'explicit' => 'boolean', - 'number' => 'integer', + 'number' => '?integer', 'season_number' => '?integer', 'type' => 'string', 'block' => 'boolean', @@ -91,9 +87,19 @@ class Episode extends Entity 'updated_by' => 'integer', ]; - public function setImage(?\CodeIgniter\HTTP\Files\UploadedFile $image) + /** + * Saves an episode image + * + * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image + * + */ + public function setImage($image) { - if (!empty($image) && $image->isValid()) { + if ( + !empty($image) && + (!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) || + $image->isValid()) + ) { // check whether the user has inputted an image and store it $this->attributes['image_uri'] = save_podcast_media( $image, @@ -136,10 +142,19 @@ class Episode extends Entity return $this->getPodcast()->image_url; } - public function setEnclosure( - \CodeIgniter\HTTP\Files\UploadedFile $enclosure = null - ) { - if (!empty($enclosure) && $enclosure->isValid()) { + /** + * Saves an enclosure + * + * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $enclosure + * + */ + public function setEnclosure($enclosure = null) + { + if ( + !empty($enclosure) && + (!($enclosure instanceof \CodeIgniter\HTTP\Files\UploadedFile) || + $enclosure->isValid()) + ) { helper('media'); $this->attributes['enclosure_uri'] = save_podcast_media( @@ -194,9 +209,11 @@ class Episode extends Entity ); } - public function getGUID() + public function setGuid($guid = null) { - return $this->getLink(); + return $this->attributes['guid'] = empty($guid) + ? $this->getLink() + : $guid; } public function getPodcast() diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index c139ef028e..916d723214 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -57,7 +57,7 @@ class Podcast extends Entity 'description' => 'string', 'image_uri' => 'string', 'language' => 'string', - 'category' => 'string', + 'category_id' => 'integer', 'explicit' => 'boolean', 'author' => '?string', 'owner_name' => '?string', @@ -70,9 +70,16 @@ class Podcast extends Entity 'custom_html_head' => '?string', 'created_by' => 'integer', 'updated_by' => 'integer', + 'imported_feed_url' => '?string', ]; - public function setImage(\CodeIgniter\HTTP\Files\UploadedFile $image = null) + /** + * Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/` + * + * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image + * + */ + public function setImage($image = null) { if ($image) { helper('media'); diff --git a/app/Helpers/media_helper.php b/app/Helpers/media_helper.php index 1f21679a84..594bb009df 100644 --- a/app/Helpers/media_helper.php +++ b/app/Helpers/media_helper.php @@ -9,7 +9,7 @@ /** * Saves a file to the corresponding podcast folder in `public/media` * - * @param \CodeIgniter\HTTP\Files\UploadedFile $file + * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $file * @param string $podcast_name * @param string $file_name * @@ -17,7 +17,12 @@ */ function save_podcast_media($file, $podcast_name, $media_name) { - $file_name = $media_name . '.' . $file->guessExtension(); + $file_name = $media_name . '.' . $file->getExtension(); + + if (!file_exists(config('App')->mediaRoot . '/' . $podcast_name)) { + mkdir(config('App')->mediaRoot . '/' . $podcast_name, 0777, true); + touch(config('App')->mediaRoot . '/' . $podcast_name . '/index.html'); + } // move to media folder and overwrite file if already existing $file->move( @@ -29,6 +34,20 @@ function save_podcast_media($file, $podcast_name, $media_name) return $podcast_name . '/' . $file_name; } +function download_file($fileUrl) +{ + $tmpFilename = + time() . + '_' . + bin2hex(random_bytes(10)) . + '.' . + pathinfo($fileUrl, PATHINFO_EXTENSION); + $tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename; + file_put_contents($tmpFilePath, file_get_contents($fileUrl)); + + return new \CodeIgniter\Files\File($tmpFilePath); +} + /** * Prefixes the root media path to a given uri * diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php index b1511ef39a..b1d1b168a3 100644 --- a/app/Helpers/misc_helper.php +++ b/app/Helpers/misc_helper.php @@ -35,3 +35,111 @@ function startsWith($string, $query) { return substr($string, 0, strlen($query)) === $query; } + +function slugify($text) +{ + if (empty($text)) { + return 'n-a'; + } + + // replace non letter or digits by - + $text = preg_replace('~[^\pL\d]+~u', '-', $text); + + $unwanted_array = [ + 'Å ' => 'S', + 'Å¡' => 's', + 'Ä' => 'Dj', + 'Ä‘' => 'dj', + 'Ž' => 'Z', + 'ž' => 'z', + 'ÄŒ' => 'C', + 'Ä' => 'c', + 'Ć' => 'C', + 'ć' => 'c', + 'À' => 'A', + 'Ã' => 'A', + 'Â' => 'A', + 'Ã' => 'A', + 'Ä' => 'A', + 'Ã…' => 'A', + 'Æ' => 'AE', + 'Ç' => 'C', + 'È' => 'E', + 'É' => 'E', + 'Ê' => 'E', + 'Ë' => 'E', + 'ÃŒ' => 'I', + 'Ã' => 'I', + 'ÃŽ' => 'I', + 'Ã' => 'I', + 'Ñ' => 'N', + 'Ã’' => 'O', + 'Ó' => 'O', + 'Ô' => 'O', + 'Õ' => 'O', + 'Ö' => 'O', + 'Ø' => 'O', + 'Å’' => 'OE', + 'Ù' => 'U', + 'Ú' => 'U', + 'Û' => 'U', + 'Ãœ' => 'U', + 'Ã' => 'Y', + 'Þ' => 'B', + 'ß' => 'Ss', + 'à ' => 'a', + 'á' => 'a', + 'â' => 'a', + 'ã' => 'a', + 'ä' => 'a', + 'Ã¥' => 'a', + 'æ' => 'ae', + 'ç' => 'c', + 'è' => 'e', + 'é' => 'e', + 'ê' => 'e', + 'ë' => 'e', + 'ì' => 'i', + 'Ã' => 'i', + 'î' => 'i', + 'ï' => 'i', + 'ð' => 'o', + 'ñ' => 'n', + 'ò' => 'o', + 'ó' => 'o', + 'ô' => 'o', + 'õ' => 'o', + 'ö' => 'o', + 'ø' => 'o', + 'Å“' => 'OE', + 'ù' => 'u', + 'ú' => 'u', + 'û' => 'u', + 'ý' => 'y', + 'ý' => 'y', + 'þ' => 'b', + 'ÿ' => 'y', + 'Å”' => 'R', + 'Å•' => 'r', + '/' => '-', + ' ' => '-', + ]; + $text = strtr($text, $unwanted_array); + + // transliterate + $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); + + // remove unwanted characters + $text = preg_replace('~[^-\w]+~', '', $text); + + // trim + $text = trim($text, '-'); + + // remove duplicate - + $text = preg_replace('~-+~', '-', $text); + + // lowercase + $text = strtolower($text); + + return $text; +} diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 58f5072628..f08297f698 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -23,7 +23,7 @@ function get_rss_feed($podcast) $episodes = $podcast->episodes; $podcast_category = $category_model - ->where('code', $podcast->category) + ->where('id', $podcast->category_id) ->first(); $itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; @@ -50,7 +50,7 @@ function get_rss_feed($podcast) ); $channel->addChild( 'generator', - 'Castopod 0.0.0-development - https://castopod.org' + 'Castopod 0.0.0-development - https://castopod.org/' ); $channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html'); diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index bd5d4b61d1..d434287bcc 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -19,4 +19,5 @@ return [ 'users' => 'users', 'my-account' => 'my account', 'change-password' => 'change password', + 'import' => 'feed import', ]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index b64c672546..8a6bda75a2 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -10,6 +10,7 @@ return [ 'all_podcasts' => 'All podcasts', 'no_podcast' => 'No podcast found!', 'create' => 'Create a Podcast', + 'import' => 'Create and Import a Podcast from an existing Feed', 'new_episode' => 'New Episode', 'feed' => 'RSS feed', 'view' => 'View podcast', @@ -21,10 +22,10 @@ return [ 'form' => [ 'title' => 'Title', 'title_help' => - 'This podcast title. It will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).', + 'The podcast title will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).', 'name' => 'Name', 'name_help' => - 'This podcast name. It will be used in the URL address. It will be used as a Fedivers actor name, (for instance, it will be the podcast Mastodon’s name).', + 'The podcast will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).', 'description' => 'Description', 'description_help' => 'It will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).', @@ -33,7 +34,7 @@ return [ 'This text will be automatically added at the end of each episode description, so that you don’t have to copy/paste it a gazillion times.', 'image' => 'Image', 'image_help' => - 'This podcast image. It should be square, JPEG or PNG, minimum 1400 x 1400 pixels and maximum 3000 x 3000 pixels.', + 'This podcast image. It must be square, JPEG or PNG, minimum 1400 x 1400 pixels and maximum 3000 x 3000 pixels.', 'language' => 'Language', 'language_help' => 'The language spoken on the podcast.', 'category' => 'Category', @@ -44,10 +45,10 @@ return [ 'The podcast parental advisory information. Does it contain explicit content?', 'owner_name' => 'Owner name', 'owner_name_help' => - 'The podcast owner contact name. For administrative use only. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', + 'For administrative use only. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', 'owner_email' => 'Owner email', 'owner_email_help' => - 'The podcast owner contact e-mail. For administrative use only. It will mostly be used by some platforms to verify this podcast ownerhip. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', + 'It will be used by most platforms to verify this podcast ownership. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', 'author' => 'Author', 'author_help' => 'The group responsible for creating the show. Show author most often refers to the parent company or network of a podcast. This field is sometimes labeled as ’Author’.', @@ -75,6 +76,36 @@ return [ 'submit_create' => 'Create podcast', 'submit_edit' => 'Save podcast', ], + 'form_import' => [ + 'name' => 'Name', + 'name_help' => + 'This podcast name. It will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).', + 'imported_feed_url' => 'Feed URL', + 'imported_feed_url_help' => + 'Make sure you are legally allowed to copy that podcast.', + 'force_renumber' => 'Force episodes renumbering', + 'force_renumber_help' => + 'Use this if your old podcast does not have number but you want some on your new one.', + 'season_number' => 'Season number', + 'season_number_help' => + 'Use this if your old podcast does not have season number but you want one on your new one. Leave blank otherwise.', + 'slug_field' => [ + 'label' => 'Which field should be used to calculate episode slug', + 'link' => '<link>', + 'title' => '<title>', + ], + 'description_field' => [ + 'label' => 'Source field used for episode description / show notes', + 'description' => '<description>', + 'summary' => '<itunes:summary>', + 'subtitle_summary' => + '<itunes:subtitle> <itunes:summary>', + ], + 'max_episodes' => 'Maximum number of episodes to import', + 'max_episodes_helper' => 'Leave blank to import all episodes', + 'submit_import' => 'Import podcast', + 'submit_importing' => 'Importing podcast, this could take a while…', + ], 'category_options' => [ 'uncategorized' => 'uncategorized', 'arts' => 'Arts', diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 9f911d770b..b346fcff64 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -17,6 +17,7 @@ class EpisodeModel extends Model protected $allowedFields = [ 'podcast_id', + 'guid', 'title', 'slug', 'enclosure_uri', @@ -44,8 +45,8 @@ class EpisodeModel extends Model 'enclosure_uri' => 'required', 'description' => 'required', 'image_uri' => 'required', - 'number' => 'required|is_natural_no_zero', - 'season_number' => 'required|is_natural_no_zero', + 'number' => 'is_natural_no_zero|permit_empty', + 'season_number' => 'is_natural_no_zero|permit_empty', 'type' => 'required', 'published_at' => 'valid_date|permit_empty', 'created_by' => 'required', diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 9a3bc6ac49..023f7a2341 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -23,7 +23,7 @@ class PodcastModel extends Model 'episode_description_footer', 'image_uri', 'language', - 'category', + 'category_id', 'explicit', 'owner_name', 'owner_email', @@ -35,6 +35,7 @@ class PodcastModel extends Model 'custom_html_head', 'created_by', 'updated_by', + 'imported_feed_url', ]; protected $returnType = \App\Entities\Podcast::class; @@ -49,8 +50,7 @@ class PodcastModel extends Model 'description' => 'required', 'image_uri' => 'required', 'language' => 'required', - 'category' => 'required', - 'owner_name' => 'required', + 'category_id' => 'required', 'owner_email' => 'required|valid_email', 'type' => 'required', 'created_by' => 'required', diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php index 7581a64831..99c597e80f 100644 --- a/app/Views/admin/podcast/edit.php +++ b/app/Views/admin/podcast/edit.php @@ -92,7 +92,7 @@ <?= form_dropdown( 'category', $categoryOptions, - old('category', $podcast->category), + old('category', $podcast->category_id), [ 'id' => 'category', 'class' => 'form-select mb-4', diff --git a/app/Views/admin/podcast/import.php b/app/Views/admin/podcast/import.php new file mode 100644 index 0000000000..5bbaf110bd --- /dev/null +++ b/app/Views/admin/podcast/import.php @@ -0,0 +1,163 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= lang('Podcast.import') ?> +<?= $this->endSection() ?> + + +<?= $this->section('content') ?> + +<?= form_open_multipart(route_to('podcast_import'), [ + 'method' => 'post', + 'class' => 'flex flex-col max-w-md', +]) ?> +<?= csrf_field() ?> + + +<div class="flex flex-col mb-4"> + <label for="name"><?= lang('Podcast.form_import.name') ?></label> + <input type="text" class="form-input" id="name" name="name" value="<?= old( + 'name' + ) ?>" required /> +</div> + +<div class="flex flex-col mb-4"> + <label for="name"><?= lang( + 'Podcast.form_import.imported_feed_url' + ) ?></label> + <input type="text" class="form-input" id="imported_feed_url" name="imported_feed_url" value="<?= old( + 'imported_feed_url' + ) ?>" required /> +</div> + +<?= form_label(lang('Podcast.form.language'), 'language') ?> +<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [ + 'id' => 'language', + 'class' => 'form-select mb-4', + 'required' => 'required', +]) ?> + +<?= form_label(lang('Podcast.form.category'), 'category') ?> +<?= form_dropdown('category', $categoryOptions, old('category'), [ + 'id' => 'category', + 'class' => 'form-select mb-4', + 'required' => 'required', +]) ?> + +<?= form_fieldset(lang('Podcast.form_import.slug_field.label'), [ + 'class' => 'flex flex-col mb-4', +]) ?> + <label for="link" class="inline-flex items-center"> + <?= form_radio( + ['id' => 'link', 'name' => 'slug_field', 'class' => 'form-radio'], + 'link', + old('slug_field') ? old('slug_field') == 'link' : true + ) ?> + <span class="ml-2"><?= lang( + 'Podcast.form_import.slug_field.link' + ) ?></span> + </label> + <label for="title" class="inline-flex items-center"> + <?= form_radio( + ['id' => 'title', 'name' => 'slug_field', 'class' => 'form-radio'], + 'title', + old('slug_field') ? old('slug_field') == 'title' : false + ) ?> + <span class="ml-2"><?= lang( + 'Podcast.form_import.slug_field.title' + ) ?></span> + </label> +<?= form_fieldset_close() ?> + +<?= form_fieldset(lang('Podcast.form_import.description_field.label'), [ + 'class' => 'flex flex-col mb-4', +]) ?> + <label for="description" class="inline-flex items-center"> + <?= form_radio( + [ + 'id' => 'description', + 'name' => 'description_field', + 'class' => 'form-radio', + ], + 'description', + old('description_field') + ? old('description_field') == 'description' + : true + ) ?> + <span class="ml-2"><?= lang( + 'Podcast.form_import.description_field.description' + ) ?></span> + </label> + <label for="subtitle_summary" class="inline-flex items-center"> + <?= form_radio( + [ + 'id' => 'summary', + 'name' => 'description_field', + 'class' => 'form-radio', + ], + 'summary', + old('description_field') + ? old('description_field') == 'summary' + : false + ) ?> + <span class="ml-2"><?= lang( + 'Podcast.form_import.description_field.summary' + ) ?></span> + </label> + <label for="subtitle_summary" class="inline-flex items-center"> + <?= form_radio( + [ + 'id' => 'subtitle_summary', + 'name' => 'description_field', + 'class' => 'form-radio', + ], + 'subtitle_summary', + old('description_field') + ? old('description_field') == 'subtitle_summary' + : false + ) ?> + <span class="ml-2"><?= lang( + 'Podcast.form_import.description_field.subtitle_summary' + ) ?></span> + </label> +<?= form_fieldset_close() ?> + + +<label class="inline-flex items-center mb-4"> + <?= form_checkbox( + [ + 'id' => 'force_renumber', + 'name' => 'force_renumber', + 'class' => 'form-checkbox', + ], + 'yes', + old('force_renumber', false) + ) ?> + <span class="ml-2"><?= lang('Podcast.form_import.force_renumber') ?></span> +</label> + +<div class="flex flex-col mb-4"> + <label for="name"><?= lang('Podcast.form_import.season_number') ?></label> + <input type="text" class="form-input" id="season_number" name="season_number" value="<?= old( + 'season_number' + ) ?>" /> +</div> + +<div class="flex flex-col mb-4"> + <label for="max_episodes"><?= lang( + 'Podcast.form_import.max_episodes' + ) ?></label> + <input type="text" class="form-input" id="max_episodes" name="max_episodes" value="<?= old( + 'max_episodes' + ) ?>" /> +</div> + +<button type="submit" name="submit" onsubmit="this.disabled=true; this.value='<?= lang( + 'Podcast.form_import.submit_importing' +) ?>';" class="self-end px-4 py-2 bg-gray-200"><?= lang( + 'Podcast.form_import.submit_import' +) ?></button> +<?= form_close() ?> + + +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php index 83768a7cbe..cc1097cf32 100644 --- a/app/Views/admin/podcast/list.php +++ b/app/Views/admin/podcast/list.php @@ -7,6 +7,11 @@ ) ?>"> <?= icon('add', 'mr-2') ?> <?= lang('Podcast.create') ?></a> +<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( + 'podcast-import' +) ?>"> +<?= icon('add', 'mr-2') ?> +<?= lang('Podcast.import') ?></a> <?= $this->endSection() ?> diff --git a/composer.json b/composer.json index 44be1c8c65..dc3cf041ac 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "myth/auth": "dev-develop", "codeigniter4/codeigniter4": "dev-develop", "league/commonmark": "^1.5", - "vlucas/phpdotenv": "^5.1" + "vlucas/phpdotenv": "^5.1", + "league/html-to-markdown": "^4.10" }, "require-dev": { "mikey179/vfsstream": "1.6.*", diff --git a/composer.lock b/composer.lock index bdeba108ec..e487409e94 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6be291e1c7f73b73182cd7b49234688", + "content-hash": "38eeae7f5d0143863430cda9df10d487", "packages": [ { "name": "codeigniter4/codeigniter4", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/codeigniter4/CodeIgniter4.git", - "reference": "cbfc8d27645fc9fe19d540c796b627852b4a1142" + "reference": "9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/cbfc8d27645fc9fe19d540c796b627852b4a1142", - "reference": "cbfc8d27645fc9fe19d540c796b627852b4a1142", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee", + "reference": "9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee", "shasum": "" }, "require": { @@ -34,6 +34,7 @@ "codeigniter4/codeigniter4-standard": "^1.0", "fzaninotto/faker": "^1.9@dev", "mikey179/vfsstream": "1.6.*", + "phpstan/phpstan": "^0.12.37", "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", "squizlabs/php_codesniffer": "^3.3" @@ -65,7 +66,7 @@ "slack": "https://codeigniterchat.slack.com", "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" }, - "time": "2020-08-04T03:43:32+00:00" + "time": "2020-08-17T14:11:23+00:00" }, { "name": "composer/ca-bundle", @@ -601,31 +602,113 @@ ], "time": "2020-07-19T22:47:30+00:00" }, + { + "name": "league/html-to-markdown", + "version": "4.10.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "0868ae7a552e809e5cd8f93ba022071640408e88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0868ae7a552e809e5cd8f93ba022071640408e88", + "reference": "0868ae7a552e809e5cd8f93ba022071640408e88", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "^4.8|^5.7", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.10-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + } + ], + "time": "2020-07-01T00:34:03+00:00" + }, { "name": "maxmind-db/reader", - "version": "v1.6.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", - "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4" + "reference": "942553da239f12051275f9c666538b5dd09e2908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/febd4920bf17c1da84cef58e56a8227dfb37fbe4", - "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/942553da239f12051275f9c666538b5dd09e2908", + "reference": "942553da239f12051275f9c666538b5dd09e2908", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.2" }, "conflict": { - "ext-maxminddb": "<1.6.0,>=2.0.0" + "ext-maxminddb": "<1.7.0,>=2.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpcov": "^3.0", - "phpunit/phpunit": "5.*", + "phpunit/phpcov": ">=6.0.0", + "phpunit/phpunit": ">=8.0.0,<10.0.0", "squizlabs/php_codesniffer": "3.*" }, "suggest": { @@ -659,7 +742,7 @@ "geolocation", "maxmind" ], - "time": "2019-12-19T22:59:03+00:00" + "time": "2020-08-07T22:10:05+00:00" }, { "name": "maxmind/web-service-common", @@ -1618,16 +1701,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.0", + "version": "5.2.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df" + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", "shasum": "" }, "require": { @@ -1666,7 +1749,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-07-20T20:05:34+00:00" + "time": "2020-08-15T11:14:08+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2026,6 +2109,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { @@ -2738,16 +2822,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.5", + "version": "3.5.6", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", "shasum": "" }, "require": { @@ -2785,7 +2869,7 @@ "phpcs", "standards" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-08-10T04:50:15+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/setup-development.md b/docs/setup-development.md index 47668c9aef..66e02a3c56 100644 --- a/docs/setup-development.md +++ b/docs/setup-development.md @@ -106,12 +106,24 @@ docker-compose run --rm app php spark migrate -all 2. Populate the database with the required data: +```bash +# Populates all required data +docker-compose run --rm app php spark db:seed AppSeeder +``` + +You may also add only data you chose: + ```bash # Populates all categories docker-compose run --rm app php spark db:seed CategorySeeder +# Populates all Languages docker-compose run --rm app php spark db:seed LanguageSeeder +# Populates all podcasts platforms docker-compose run --rm app php spark db:seed PlatformSeeder +# Populates all Authentication data (roles definition…) docker-compose run --rm app php spark db:seed AuthSeeder +# Populates test data (login: admin / password: AGUehL3P) +docker-compose run --rm app php spark db:seed TestSeeder ``` 3. (optionnal) Populate the database with test data: -- GitLab