feat(person): add podcastindex.org namespace person tag

This commit is contained in:
Benjamin Bellamy 2021-02-10 16:20:01 +00:00
parent 17e1e94a50
commit 8acd011f13
61 changed files with 2815 additions and 101 deletions

12
.gitignore vendored
View File

@ -30,6 +30,9 @@ $RECYCLE.BIN/
# Linux
*~
# vim
*.swp
# KDE directory preferences
.directory
@ -135,6 +138,7 @@ node_modules
# public folder
public/*
!public/media
!public/media/~person
!public/.htaccess
!public/favicon.ico
!public/index.php
@ -144,6 +148,14 @@ public/*
public/media/*
!public/media/index.html
# public person folder
public/media/~person/*
!public/media/~person/index.html
# Generated files
app/Language/en/PersonsTaxonomy.php
app/Language/fr/PersonsTaxonomy.php
#-------------------------
# Docker volumes
#-------------------------

View File

@ -85,6 +85,37 @@ $routes->group(
'as' => 'my-podcasts',
]);
$routes->group('persons', function ($routes) {
$routes->get('/', 'Person', [
'as' => 'person-list',
'filter' => 'permission:person-list',
]);
$routes->get('new', 'Person::create', [
'as' => 'person-create',
'filter' => 'permission:person-create',
]);
$routes->post('new', 'Person::attemptCreate', [
'filter' => 'permission:person-create',
]);
$routes->group('(:num)', function ($routes) {
$routes->get('/', 'Person::view/$1', [
'as' => 'person-view',
'filter' => 'permission:person-view',
]);
$routes->get('edit', 'Person::edit/$1', [
'as' => 'person-edit',
'filter' => 'permission:person-edit',
]);
$routes->post('edit', 'Person::attemptEdit/$1', [
'filter' => 'permission:person-edit',
]);
$routes->add('delete', 'Person::delete/$1', [
'as' => 'person-delete',
'filter' => 'permission:person-delete',
]);
});
});
// Podcasts
$routes->group('podcasts', function ($routes) {
$routes->get('/', 'Podcast::list', [
@ -124,6 +155,25 @@ $routes->group(
'filter' => 'permission:podcasts-delete',
]);
$routes->group('persons', function ($routes) {
$routes->get('/', 'PodcastPerson/$1', [
'as' => 'podcast-person-manage',
'filter' => 'permission:podcast-edit',
]);
$routes->post('/', 'PodcastPerson::attemptAdd/$1', [
'filter' => 'permission:podcast-edit',
]);
$routes->get(
'(:num)/remove',
'PodcastPerson::remove/$1/$2',
[
'as' => 'podcast-person-remove',
'filter' => 'permission:podcast-edit',
]
);
});
$routes->group('analytics', function ($routes) {
$routes->get('/', 'Podcast::viewAnalytics/$1', [
'as' => 'podcast-analytics',
@ -276,6 +326,30 @@ $routes->group(
'filter' => 'permission:podcast_episodes-edit',
]
);
$routes->group('persons', function ($routes) {
$routes->get('/', 'EpisodePerson/$1/$2', [
'as' => 'episode-person-manage',
'filter' => 'permission:podcast_episodes-edit',
]);
$routes->post(
'/',
'EpisodePerson::attemptAdd/$1/$2',
[
'filter' =>
'permission:podcast_episodes-edit',
]
);
$routes->get(
'(:num)/remove',
'EpisodePerson::remove/$1/$2/$3',
[
'as' => 'episode-person-remove',
'filter' =>
'permission:podcast_episodes-edit',
]
);
});
});
});
@ -497,6 +571,7 @@ $routes->group('@(:podcastName)', function ($routes) {
$routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
$routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
});
$routes->get('/credits', 'Page::credits', ['as' => 'credits']);
$routes->get('/(:slug)', 'Page/$1', ['as' => 'page']);
/**

View File

@ -0,0 +1,111 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\EpisodePersonModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
class EpisodePerson extends BaseController
{
/**
* @var \App\Entities\Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode
*/
protected $episode;
public function _remap($method, ...$params)
{
if (count($params) > 1) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (
!($this->episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
} else {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[1]);
unset($params[0]);
return $this->$method(...$params);
}
public function index()
{
helper('form');
$data = [
'episode' => $this->episode,
'podcast' => $this->podcast,
'episodePersons' => (new EpisodePersonModel())->getPersonsByEpisodeId(
$this->podcast->id,
$this->episode->id
),
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/person', $data);
}
public function attemptAdd()
{
$rules = [
'person' => 'required',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new EpisodePersonModel())->addEpisodePersons(
$this->podcast->id,
$this->episode->id,
$this->request->getPost('person'),
$this->request->getPost('person_group_role')
);
return redirect()->back();
}
public function remove($episodePersonId)
{
(new EpisodePersonModel())->removeEpisodePersons(
$this->podcast->id,
$this->episode->id,
$episodePersonId
);
return redirect()->back();
}
}

View File

@ -0,0 +1,147 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PersonModel;
class Person extends BaseController
{
/**
* @var \App\Entities\Person|null
*/
protected $person;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->person = (new PersonModel())->getPersonById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function index()
{
$data = ['persons' => (new PersonModel())->findAll()];
return view('admin/person/list', $data);
}
public function view()
{
$data = ['person' => $this->person];
replace_breadcrumb_params([0 => $this->person->full_name]);
return view('admin/person/view', $data);
}
public function create()
{
helper(['form']);
return view('admin/person/create');
}
public function attemptCreate()
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$person = new \App\Entities\Person([
'full_name' => $this->request->getPost('full_name'),
'unique_name' => $this->request->getPost('unique_name'),
'information_url' => $this->request->getPost('information_url'),
'image' => $this->request->getFile('image'),
'created_by' => user()->id,
'updated_by' => user()->id,
]);
$personModel = new PersonModel();
if (!$personModel->insert($person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-list');
}
public function edit()
{
helper('form');
$data = [
'person' => $this->person,
];
replace_breadcrumb_params([0 => $this->person->full_name]);
return view('admin/person/edit', $data);
}
public function attemptEdit()
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->person->full_name = $this->request->getPost('full_name');
$this->person->unique_name = $this->request->getPost('unique_name');
$this->person->information_url = $this->request->getPost(
'information_url'
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->person->image = $image;
}
$this->updated_by = user();
$personModel = new PersonModel();
if (!$personModel->update($this->person->id, $this->person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-view', [$this->person->id]);
}
public function delete()
{
(new PersonModel())->delete($this->person->id);
return redirect()->route('person-list');
}
}

View File

@ -13,6 +13,9 @@ use App\Models\LanguageModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use App\Models\PlatformModel;
use App\Models\PersonModel;
use App\Models\PodcastPersonModel;
use App\Models\EpisodePersonModel;
use Config\Services;
use League\HTMLToMarkdown\HtmlConverter;
@ -150,7 +153,7 @@ class PodcastImport extends BaseController
: $nsItunes->complete === 'yes',
'location_name' => !$nsPodcast->location
? null
: $nsPodcast->location->attributes()['name'],
: $nsPodcast->location,
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
@ -158,9 +161,9 @@ class PodcastImport extends BaseController
: $nsPodcast->location->attributes()['geo'],
'location_osmid' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['osmid'])
empty($nsPodcast->location->attributes()['osm'])
? null
: $nsPodcast->location->attributes()['osmid'],
: $nsPodcast->location->attributes()['osm'],
'created_by' => user(),
'updated_by' => user(),
]);
@ -200,40 +203,40 @@ class PodcastImport extends BaseController
$podcastAdminGroup->id
);
$platformModel = new PlatformModel();
$podcastsPlatformsData = [];
foreach ($nsPodcast->id as $podcastingPlatform) {
$slug = $podcastingPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'podcasting');
array_push($podcastsPlatformsData, [
'platform_slug' => $slug,
'podcast_id' => $newPodcastId,
'link_url' => $podcastingPlatform->attributes()['url'],
'link_content' => $podcastingPlatform->attributes()['id'],
'is_visible' => false,
]);
}
foreach ($nsPodcast->social as $socialPlatform) {
$slug = $socialPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'social');
array_push($podcastsPlatformsData, [
'platform_slug' => $socialPlatform->attributes()['platform'],
'podcast_id' => $newPodcastId,
'link_url' => $socialPlatform->attributes()['url'],
'link_content' => $socialPlatform,
'is_visible' => false,
]);
}
foreach ($nsPodcast->funding as $fundingPlatform) {
$slug = $fundingPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'funding');
array_push($podcastsPlatformsData, [
'platform_slug' => $fundingPlatform->attributes()['platform'],
'podcast_id' => $newPodcastId,
'link_url' => $fundingPlatform->attributes()['url'],
'link_content' => $fundingPlatform->attributes()['id'],
'is_visible' => false,
]);
$platformTypes = [
['name' => 'podcasting', 'elements' => $nsPodcast->id],
['name' => 'social', 'elements' => $nsPodcast->social],
['name' => 'funding', 'elements' => $nsPodcast->funding],
];
$platformModel = new PlatformModel();
foreach ($platformTypes as $platformType) {
foreach ($platformType['elements'] as $platform) {
$platformLabel = $platform->attributes()['platform'];
$platformSlug = slugify($platformLabel);
if (!$platformModel->getPlatform($platformSlug)) {
if (
!$platformModel->createPlatform(
$platformSlug,
$platformType['name'],
$platformLabel,
''
)
) {
return redirect()
->back()
->withInput()
->with('errors', $platformModel->errors());
}
}
array_push($podcastsPlatformsData, [
'platform_slug' => $platformSlug,
'podcast_id' => $newPodcastId,
'link_url' => $platform->attributes()['url'],
'link_content' => $platform->attributes()['id'],
'is_visible' => false,
]);
}
}
if (count($podcastsPlatformsData) > 1) {
$platformModel->createPodcastPlatforms(
@ -242,6 +245,54 @@ class PodcastImport extends BaseController
);
}
foreach ($nsPodcast->person as $podcastPerson) {
$personModel = new PersonModel();
$newPersonId = null;
if ($newPerson = $personModel->getPerson($podcastPerson)) {
$newPersonId = $newPerson->id;
} else {
if (
!($newPersonId = $personModel->createPerson(
$podcastPerson,
$podcastPerson->attributes()['href'],
$podcastPerson->attributes()['img']
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
$personGroup = empty($podcastPerson->attributes()['group'])
? ['slug' => '']
: \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
(string) $podcastPerson->attributes()['group']
];
$personRole =
empty($podcastPerson->attributes()['role']) ||
empty($personGroup)
? ['slug' => '']
: $personGroup['roles'][
strval($podcastPerson->attributes()['role'])
];
$newPodcastPerson = new \App\Entities\PodcastPerson([
'podcast_id' => $newPodcastId,
'person_id' => $newPersonId,
'person_group' => $personGroup['slug'],
'person_role' => $personRole['slug'],
]);
$podcastPersonModel = new PodcastPersonModel();
if (!$podcastPersonModel->insert($newPodcastPerson)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastPersonModel->errors());
}
}
$numberItems = $feed->channel[0]->item->count();
$lastItem =
!empty($this->request->getPost('max_episodes')) &&
@ -251,6 +302,7 @@ class PodcastImport extends BaseController
$slugs = [];
//////////////////////////////////////////////////////////////////
// For each Episode:
for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) {
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
@ -326,7 +378,7 @@ class PodcastImport extends BaseController
: $nsItunes->block === 'yes',
'location_name' => !$nsPodcast->location
? null
: $nsPodcast->location->attributes()['name'],
: $nsPodcast->location,
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
@ -334,9 +386,9 @@ class PodcastImport extends BaseController
: $nsPodcast->location->attributes()['geo'],
'location_osmid' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['osmid'])
empty($nsPodcast->location->attributes()['osm'])
? null
: $nsPodcast->location->attributes()['osmid'],
: $nsPodcast->location->attributes()['osm'],
'created_by' => user(),
'updated_by' => user(),
'published_at' => strtotime($item->pubDate),
@ -344,13 +396,62 @@ class PodcastImport extends BaseController
$episodeModel = new EpisodeModel();
if (!$episodeModel->insert($newEpisode)) {
if (!($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
foreach ($nsPodcast->person as $episodePerson) {
$personModel = new PersonModel();
$newPersonId = null;
if ($newPerson = $personModel->getPerson($episodePerson)) {
$newPersonId = $newPerson->id;
} else {
if (
!($newPersonId = $personModel->createPerson(
$episodePerson,
$episodePerson->attributes()['href'],
$episodePerson->attributes()['img']
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
$personGroup = empty($episodePerson->attributes()['group'])
? ['slug' => '']
: \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
strval($episodePerson->attributes()['group'])
];
$personRole =
empty($episodePerson->attributes()['role']) ||
empty($personGroup)
? ['slug' => '']
: $personGroup['roles'][
strval($episodePerson->attributes()['role'])
];
$newEpisodePerson = new \App\Entities\PodcastPerson([
'podcast_id' => $newPodcastId,
'episode_id' => $newEpisodeId,
'person_id' => $newPersonId,
'person_group' => $personGroup['slug'],
'person_role' => $personRole['slug'],
]);
$episodePersonModel = new EpisodePersonModel();
if (!$episodePersonModel->insert($newEpisodePerson)) {
return redirect()
->back()
->withInput()
->with('errors', $episodePersonModel->errors());
}
}
}
$db->transComplete();

View File

@ -0,0 +1,89 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PodcastPersonModel;
use App\Models\PodcastModel;
use App\Models\PersonModel;
class PodcastPerson extends BaseController
{
/**
* @var \App\Entities\Podcast
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
} else {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[0]);
return $this->$method(...$params);
}
public function index()
{
helper('form');
$data = [
'podcast' => $this->podcast,
'podcastPersons' => (new PodcastPersonModel())->getPersonsByPodcastId(
$this->podcast->id
),
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/person', $data);
}
public function attemptAdd()
{
$rules = [
'person' => 'required',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new PodcastPersonModel())->addPodcastPersons(
$this->podcast->id,
$this->request->getPost('person'),
$this->request->getPost('person_group_role')
);
return redirect()->back();
}
public function remove($podcastPersonId)
{
(new PodcastPersonModel())->removePodcastPersons(
$this->podcast->id,
$podcastPersonId
);
return redirect()->back();
}
}

View File

@ -54,11 +54,55 @@ class Episode extends BaseController
$this->podcast->type
);
$persons = [];
foreach ($this->episode->episode_persons as $episodePerson) {
if (array_key_exists($episodePerson->person->id, $persons)) {
$persons[$episodePerson->person->id]['roles'] .=
empty($episodePerson->person_group) ||
empty($episodePerson->person_role)
? ''
: (empty(
$persons[$episodePerson->person->id][
'roles'
]
)
? ''
: ', ') .
lang(
'PersonsTaxonomy.persons.' .
$episodePerson->person_group .
'.roles.' .
$episodePerson->person_role .
'.label'
);
} else {
$persons[$episodePerson->person->id] = [
'full_name' => $episodePerson->person->full_name,
'information_url' =>
$episodePerson->person->information_url,
'thumbnail_url' =>
$episodePerson->person->image->thumbnail_url,
'roles' =>
empty($episodePerson->person_group) ||
empty($episodePerson->person_role)
? ''
: lang(
'PersonsTaxonomy.persons.' .
$episodePerson->person_group .
'.roles.' .
$episodePerson->person_role .
'.label'
),
];
}
}
$data = [
'previousEpisode' => $previousNextEpisodes['previous'],
'nextEpisode' => $previousNextEpisodes['next'],
'podcast' => $this->podcast,
'episode' => $this->episode,
'persons' => $persons,
];
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(

View File

@ -9,6 +9,8 @@
namespace App\Controllers;
use App\Models\PageModel;
use App\Models\CreditModel;
use App\Models\PodcastModel;
class Page extends BaseController
{
@ -42,4 +44,137 @@ class Page extends BaseController
];
return view('page', $data);
}
public function credits()
{
$locale = service('request')->getLocale();
$model = new PodcastModel();
$allPodcasts = $model->findAll();
if (!($found = cache("credits_{$locale}"))) {
$page = new \App\Entities\Page([
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'content' => '',
]);
$creditModel = (new CreditModel())->findAll();
// Unlike the carpenter, we make a tree from a table:
$person_group = null;
$person_id = null;
$person_role = null;
$credits = [];
foreach ($creditModel as $credit) {
if ($person_group !== $credit->person_group) {
$person_group = $credit->person_group;
$person_id = $credit->person_id;
$person_role = $credit->person_role;
$credits[$person_group] = [
'group_label' => $credit->group_label,
'persons' => [
$person_id => [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' =>
$credit->person->information_url,
'roles' => [
$person_role => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
: $credit->podcast->title,
],
],
],
],
],
],
];
} elseif ($person_id !== $credit->person_id) {
$person_id = $credit->person_id;
$person_role = $credit->person_role;
$credits[$person_group]['persons'][$person_id] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' => $credit->person->information_url,
'roles' => [
$person_role => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
: $credit->podcast->title,
],
],
],
],
];
} elseif ($person_role !== $credit->person_role) {
$person_role = $credit->person_role;
$credits[$person_group]['persons'][$person_id]['roles'][
$person_role
] = [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
: $credit->podcast->title,
],
],
];
} else {
$credits[$person_group]['persons'][$person_id]['roles'][
$person_role
]['is_in'][] = [
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .
"(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}"
: $credit->podcast->title,
];
}
}
$data = [
'page' => $page,
'credits' => $credits,
];
$found = view('credits', $data);
cache()->save("credits_{$locale}", $found, DECADE);
}
return $found;
}
}

View File

@ -109,6 +109,49 @@ class Podcast extends BaseController
]);
}
$persons = [];
foreach ($this->podcast->podcast_persons as $podcastPerson) {
if (array_key_exists($podcastPerson->person->id, $persons)) {
$persons[$podcastPerson->person->id]['roles'] .=
empty($podcastPerson->person_group) ||
empty($podcastPerson->person_role)
? ''
: (empty(
$persons[$podcastPerson->person->id][
'roles'
]
)
? ''
: ', ') .
lang(
'PersonsTaxonomy.persons.' .
$podcastPerson->person_group .
'.roles.' .
$podcastPerson->person_role .
'.label'
);
} else {
$persons[$podcastPerson->person->id] = [
'full_name' => $podcastPerson->person->full_name,
'information_url' =>
$podcastPerson->person->information_url,
'thumbnail_url' =>
$podcastPerson->person->image->thumbnail_url,
'roles' =>
empty($podcastPerson->person_group) ||
empty($podcastPerson->person_role)
? ''
: lang(
'PersonsTaxonomy.persons.' .
$podcastPerson->person_group .
'.roles.' .
$podcastPerson->person_role .
'.label'
),
];
}
}
$data = [
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
@ -119,6 +162,7 @@ class Podcast extends BaseController
$yearQuery,
$seasonQuery
),
'personArray' => $persons,
];
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(

View File

@ -41,11 +41,9 @@ class AddPlatforms extends Migration
'default' => null,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
'`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()'
);
$this->forge->addKey('slug', true);
$this->forge->createTable('platforms');

View File

@ -40,12 +40,6 @@ class AddPodcastsPlatforms extends Migration
'constraint' => 1,
'default' => 0,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);

View File

@ -0,0 +1,74 @@
<?php
/**
* Class Persons
* Creates persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPersons extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'full_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the full name or alias of the person.',
],
'unique_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the slug name or alias of the person.',
'unique' => true,
],
'information_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'image_uri' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
}
public function down()
{
$this->forge->dropTable('persons');
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Class AddPodcastsPersons
* Creates podcasts_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsPersons extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey([
'podcast_id',
'person_id',
'person_group',
'person_role',
]);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->addForeignKey('person_id', 'persons', 'id');
$this->forge->createTable('podcasts_persons');
}
public function down()
{
$this->forge->dropTable('podcasts_persons');
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Class AddEpisodesPersons
* Creates episodes_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodesPersons extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey([
'podcast_id',
'episode_id',
'person_id',
'person_group',
'person_role',
]);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->addForeignKey('episode_id', 'episodes', 'id');
$this->forge->addForeignKey('person_id', 'persons', 'id');
$this->forge->createTable('episodes_persons');
}
public function down()
{
$this->forge->dropTable('episodes_persons');
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Class AddCreditView
* Creates Credit View in database
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreditView extends Migration
{
public function up()
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personTable = $this->db->prefixTable('persons');
$podcastPersonTable = $this->db->prefixTable('podcasts_persons');
$episodePersonTable = $this->db->prefixTable('episodes_persons');
$createQuery = <<<EOD
CREATE VIEW `$viewName` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable`
INNER JOIN `$personTable`
ON (`person_id`=`$personTable`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable`
INNER JOIN `$personTable`
ON (`person_id`=`$personTable`.`id`)
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
EOD;
$this->db->query($createQuery);
}
public function down()
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `$viewName`");
}
}

View File

@ -198,6 +198,33 @@ class AuthSeeder extends Seeder
'has_permission' => ['podcast_admin'],
],
],
'person' => [
[
'name' => 'create',
'description' => 'Add a new person',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all persons',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any person',
'has_permission' => ['superadmin'],
],
[
'name' => 'edit',
'description' => 'Edit a person',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' => 'Delete any person from the database',
'has_permission' => ['superadmin'],
],
],
];
static function getGroupIdByName($name, $dataGroups)

View File

@ -47,6 +47,13 @@ class PlatformSeeder extends Seeder
'home_url' => 'https://www.blubrry.com/',
'submit_url' => 'https://www.blubrry.com/addpodcast.php',
],
[
'slug' => 'breaker',
'type' => 'podcasting',
'label' => 'Breaker',
'home_url' => 'https://www.breaker.audio/',
'submit_url' => 'https://podcasters.breaker.audio/',
],
[
'slug' => 'castbox',
'type' => 'podcasting',

94
app/Entities/Credit.php Normal file
View File

@ -0,0 +1,94 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use CodeIgniter\Entity;
class Credit extends Entity
{
/**
* @var \App\Entities\Person
*/
protected $person;
/**
* @var \App\Entities\Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode
*/
protected $episode;
/**
* @var string
*/
protected $group_label;
/**
* @var string
*/
protected $role_label;
public function getPodcast()
{
return (new PodcastModel())->getPodcastById(
$this->attributes['podcast_id']
);
}
public function getEpisode()
{
if (empty($this->attributes['episode_id'])) {
return null;
} else {
return (new EpisodeModel())->getEpisodeById(
$this->attributes['podcast_id'],
$this->attributes['episode_id']
);
}
}
public function getPerson()
{
return (new PersonModel())->getPersonById(
$this->attributes['person_id']
);
}
public function getGroupLabel()
{
if (empty($this->attributes['person_group'])) {
return null;
} else {
return lang(
"PersonsTaxonomy.persons.{$this->attributes['person_group']}.label"
);
}
}
public function getRoleLabel()
{
if (
empty($this->attributes['person_group']) ||
empty($this->attributes['person_role'])
) {
return null;
} else {
return lang(
"PersonsTaxonomy.persons.{$this->attributes['person_group']}.roles.{$this->attributes['person_role']}.label"
);
}
}
}

View File

@ -10,6 +10,7 @@ namespace App\Entities;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\EpisodePersonModel;
use CodeIgniter\Entity;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
@ -76,6 +77,11 @@ class Episode extends Entity
*/
protected $chapters_url;
/**
* @var \App\Entities\EpisodePerson[]
*/
protected $episode_persons;
/**
* @var \App\Entities\Soundbite[]
*/
@ -358,6 +364,29 @@ class Episode extends Entity
: null;
}
/**
* Returns the episode's persons
*
* @return \App\Entities\EpisodePerson[]
*/
public function getEpisodePersons()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Episode must be created before getting persons.'
);
}
if (empty($this->episode_persons)) {
$this->episode_persons = (new EpisodePersonModel())->getPersonsByEpisodeId(
$this->podcast_id,
$this->id
);
}
return $this->episode_persons;
}
/**
* Returns the episodes soundbites
*

View File

@ -0,0 +1,36 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
use App\Models\PersonModel;
class EpisodePerson extends Entity
{
/**
* @var \App\Entities\Person
*/
protected $person;
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'episode_id' => 'integer',
'person_id' => 'integer',
'person_group' => '?string',
'person_role' => '?string',
];
public function getPerson()
{
return (new PersonModel())->getPersonById(
$this->attributes['person_id']
);
}
}

59
app/Entities/Person.php Normal file
View File

@ -0,0 +1,59 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
class Person extends Entity
{
/**
* @var \App\Entities\Image
*/
protected $image;
protected $casts = [
'id' => 'integer',
'full_name' => 'string',
'unique_name' => 'string',
'information_url' => '?string',
'image_uri' => 'string',
'created_by' => 'integer',
'updated_by' => 'integer',
];
/**
* Saves a picture in `public/media/~person/`
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
*
*/
public function setImage($image = null)
{
if ($image) {
helper('media');
$this->attributes['image_uri'] = save_podcast_media(
$image,
'~person',
$this->attributes['unique_name']
);
$this->image = new \App\Entities\Image(
$this->attributes['image_uri']
);
$this->image->saveSizes();
}
return $this;
}
public function getImage()
{
return new \App\Entities\Image($this->attributes['image_uri']);
}
}

View File

@ -11,6 +11,7 @@ namespace App\Entities;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\PlatformModel;
use App\Models\PodcastPersonModel;
use CodeIgniter\Entity;
use App\Models\UserModel;
use League\CommonMark\CommonMarkConverter;
@ -32,6 +33,11 @@ class Podcast extends Entity
*/
protected $episodes;
/**
* @var \App\Entities\PodcastPerson[]
*/
protected $podcast_persons;
/**
* @var \App\Entities\Category
*/
@ -167,6 +173,28 @@ class Podcast extends Entity
return $this->episodes;
}
/**
* Returns the podcast's persons
*
* @return \App\Entities\PodcastPerson[]
*/
public function getPodcastPersons()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting persons.'
);
}
if (empty($this->podcast_persons)) {
$this->podcast_persons = (new PodcastPersonModel())->getPersonsByPodcastId(
$this->id
);
}
return $this->podcast_persons;
}
/**
* Returns the podcast category entity
*

View File

@ -0,0 +1,35 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
use App\Models\PersonModel;
class PodcastPerson extends Entity
{
/**
* @var \App\Entities\Person
*/
protected $person;
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'person_id' => 'integer',
'person_group' => '?string',
'person_role' => '?string',
];
public function getPerson()
{
return (new PersonModel())->getPersonById(
$this->attributes['person_id']
);
}
}

View File

@ -20,6 +20,9 @@ function render_page_links($class = null)
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
]);
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',

View File

@ -68,18 +68,14 @@ function get_rss_feed($podcast, $serviceSlug = '')
if (!empty($podcast->location_name)) {
$locationElement = $channel->addChild(
'location',
null,
htmlspecialchars($podcast->location_name),
$podcast_namespace
);
$locationElement->addAttribute(
'name',
htmlspecialchars($podcast->location_name)
);
if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $podcast->location_geo);
}
if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osmid', $podcast->location_osmid);
$locationElement->addAttribute('osm', $podcast->location_osmid);
}
}
if (!empty($podcast->payment_pointer)) {
@ -105,7 +101,7 @@ function get_rss_feed($podcast, $serviceSlug = '')
)
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) {
$channel->addChildWithCDATA(
$channel->addChild(
'previousUrl',
$podcast->imported_feed_url,
$podcast_namespace
@ -169,6 +165,51 @@ function get_rss_feed($podcast, $serviceSlug = '')
}
}
foreach ($podcast->podcast_persons as $podcastPerson) {
$podcastPersonElement = $channel->addChild(
'person',
htmlspecialchars($podcastPerson->person->full_name),
$podcast_namespace
);
if (
!empty($podcastPerson->person_role) &&
!empty($podcastPerson->person_group)
) {
$podcastPersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
[],
'en'
)
)
);
}
if (!empty($podcastPerson->person_group)) {
$podcastPersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
[],
'en'
)
)
);
}
$podcastPersonElement->addAttribute(
'img',
$podcastPerson->person->image->large_url
);
if (!empty($podcastPerson->person->information_url)) {
$podcastPersonElement->addAttribute(
'href',
$podcastPerson->person->information_url
);
}
}
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
@ -222,21 +263,14 @@ function get_rss_feed($podcast, $serviceSlug = '')
if (!empty($episode->location_name)) {
$locationElement = $item->addChild(
'location',
null,
htmlspecialchars($episode->location_name),
$podcast_namespace
);
$locationElement->addAttribute(
'name',
htmlspecialchars($episode->location_name)
);
if (!empty($episode->location_geo)) {
$locationElement->addAttribute('geo', $episode->location_geo);
}
if (!empty($episode->location_osmid)) {
$locationElement->addAttribute(
'osmid',
$episode->location_osmid
);
$locationElement->addAttribute('osm', $episode->location_osmid);
}
}
$item->addChildWithCDATA('description', $episode->description_html);
@ -312,6 +346,51 @@ function get_rss_feed($podcast, $serviceSlug = '')
$soundbiteElement->addAttribute('duration', $soundbite->duration);
}
foreach ($episode->episode_persons as $episodePerson) {
$episodePersonElement = $item->addChild(
'person',
htmlspecialchars($episodePerson->person->full_name),
$podcast_namespace
);
if (
!empty($episodePerson->person_role) &&
!empty($episodePerson->person_group)
) {
$episodePersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
[],
'en'
)
)
);
}
if (!empty($episodePerson->person_group)) {
$episodePersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
[],
'en'
)
)
);
}
$episodePersonElement->addAttribute(
'img',
$episodePerson->person->image->large_url
);
if (!empty($episodePerson->person->information_url)) {
$episodePersonElement->addAttribute(
'href',
$episodePerson->person->information_url
);
}
}
$episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace);
}

View File

@ -14,6 +14,9 @@ return [
'podcast-list' => 'All podcasts',
'podcast-create' => 'New podcast',
'podcast-import' => 'Import a podcast',
'persons' => 'Persons',
'person-list' => 'All persons',
'person-create' => 'New person',
'users' => 'Users',
'user-list' => 'All users',
'user-create' => 'New user',

View File

@ -16,6 +16,7 @@ return [
'add' => 'add',
'new' => 'new',
'edit' => 'edit',
'persons' => 'persons',
'users' => 'users',
'my-account' => 'my account',
'change-password' => 'change password',

View File

@ -0,0 +1,64 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'persons' => 'Persons',
'all_persons' => 'All persons',
'no_person' => 'Nobody found!',
'create' => 'Create a person',
'view' => 'View person',
'edit' => 'Edit person',
'delete' => 'Delete person',
'form' => [
'identity_section_title' => 'Identity',
'identity_section_subtitle' => 'Who is working on the podcast',
'full_name' => 'Full name',
'full_name_hint' => 'This is the full name or alias of the person.',
'unique_name' => 'Unique name',
'unique_name_hint' => 'Used for URLs',
'information_url' => 'Information URL',
'information_url_hint' =>
'Url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'image' => 'Picture, avatar, image',
'image_size_hint' =>
'Image must be squared with at least 400px wide and tall.',
'submit_create' => 'Create person',
'submit_edit' => 'Save person',
],
'podcast_form' => [
'title' => 'Manage persons',
'manage_section_title' => 'Management',
'manage_section_subtitle' => 'Remove persons from this podcast',
'add_section_title' => 'Add persons to this podcast',
'add_section_subtitle' => 'You may pick several persons and roles.',
'person' => 'Persons',
'person_hint' =>
'You may select one or several persons with the same roles. You need to create the persons first.',
'group_role' => 'Groups and roles',
'group_role_hint' =>
'You may select none, one or several groups and roles for a person.',
'submit_add' => 'Add person(s)',
'remove' => 'Remove',
],
'episode_form' => [
'title' => 'Manage persons',
'manage_section_title' => 'Management',
'manage_section_subtitle' => 'Remove persons from this episode',
'add_section_title' => 'Add persons to this episode',
'add_section_subtitle' => 'You may pick several persons and roles',
'person' => 'Persons',
'person_hint' =>
'You may select one or several persons with the same roles. You need to create the persons first.',
'group_role' => 'Groups and roles',
'group_role_hint' =>
'You may select none, one or several groups and roles for a person.',
'submit_add' => 'Add person(s)',
'remove' => 'Remove',
],
'credits' => 'Credits',
];

View File

@ -27,7 +27,8 @@ return [
'image' => 'Cover image',
'title' => 'Title',
'name' => 'Name',
'name_hint' => 'Used for generating the podcast URL.',
'name_hint' =>
'Used for generating the podcast URL. Uppercase, lowercase, numbers and underscores are accepted.',
'type' => [
'label' => 'Type',
'hint' =>

View File

@ -15,6 +15,8 @@ return [
'episode-list' => 'All episodes',
'episode-create' => 'New episode',
'analytics' => 'Analytics',
'persons' => 'Persons',
'podcast-person-manage' => 'Manage persons',
'contributors' => 'Contributors',
'contributor-list' => 'All contributors',
'contributor-add' => 'Add contributor',

View File

@ -14,6 +14,9 @@ return [
'podcast-list' => 'Tous les podcasts',
'podcast-create' => 'Créer un podcast',
'podcast-import' => 'Importer un podcast',
'persons' => 'Intervenants',
'person-list' => 'Tous les intervenants',
'person-create' => 'Nouvel intervenant',
'users' => 'Utilisateurs',
'user-list' => 'Tous les utilisateurs',
'user-create' => 'Créer un utilisateur',

View File

@ -16,6 +16,7 @@ return [
'add' => 'ajouter',
'new' => 'créer',
'edit' => 'modifier',
'persons' => 'intervenants',
'users' => 'utilisateurs',
'my-account' => 'mon compte',
'change-password' => 'changer le mot de passe',

View File

@ -0,0 +1,66 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'persons' => 'Intervenants',
'all_persons' => 'Tous les intervenants',
'no_person' => 'Aucun intervenant trouvé!',
'create' => 'Créer un intervenant',
'view' => 'Voir lintervenant',
'edit' => 'Modifier lintervenant',
'delete' => 'Supprimer lintervenant',
'form' => [
'identity_section_title' => 'Identité',
'identity_section_subtitle' => 'Qui intervient sur le podcast',
'full_name' => 'Nom complet',
'full_name_hint' => 'Le nom complet ou le pseudonyme de lintervenant',
'unique_name' => 'Nom unique',
'unique_name_hint' => 'Utilisé pour les URLs',
'information_url' => 'Adresse dinformation',
'information_url_hint' =>
'URL pointant vers des informations relatives à lintervenant, telle quune page personnelle ou une page de profil sur une plateforme tierce.',
'image' => 'Photo, avatar, image',
'image_size_hint' =>
'Limage doit être carrée et avoir au moins 400px de largeur et de hauteur.',
'submit_create' => 'Créer lintervenant',
'submit_edit' => 'Enregistrer lintervenant',
],
'podcast_form' => [
'title' => 'Gérer les intervenants',
'manage_section_title' => 'Gestion',
'manage_section_subtitle' => 'Retirer des intervenants de ce podcast',
'add_section_title' => 'Ajouter des intervenants à ce podcast',
'add_section_subtitle' =>
'Vous pouvez sélectionner plusieurs intervenants et rôles.',
'person' => 'Intervenants',
'person_hint' =>
'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.',
'group_role' => 'Groupes et rôles',
'group_role_hint' =>
'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.',
'submit_add' => 'Ajouter un/des intervenant(s)',
'remove' => 'Retirer',
],
'episode_form' => [
'title' => 'Gérer les intervenants',
'manage_section_title' => 'Gestion',
'manage_section_subtitle' => 'Retirer des intervenants de cet épisode',
'add_section_title' => 'Ajouter des intervenants à cet épisode',
'add_section_subtitle' =>
'Vous pouvez sélectionner plusieurs intervenants et rôles.',
'person' => 'Intervenants',
'person_hint' =>
'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.',
'group_role' => 'Groupes et rôles',
'group_role_hint' =>
'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.',
'submit_add' => 'Ajouter un/des intervenant(s)',
'remove' => 'Retirer',
],
'credits' => 'Crédits',
];

View File

@ -28,7 +28,8 @@ return [
'image' => 'Image de couverture',
'title' => 'Titre',
'name' => 'Nom',
'name_hint' => 'Utilisé pour ladresse du podcast.',
'name_hint' =>
'Utilisé pour ladresse du podcast. Les majuscules, les minuscules, les chiffres et le caractère souligné «_» sont acceptés.',
'type' => [
'label' => 'Type',
'hint' =>

View File

@ -15,6 +15,8 @@ return [
'episode-list' => 'Tous les épisodes',
'episode-create' => 'Créer un épisode',
'analytics' => 'Mesures daudience',
'persons' => 'Intervenants',
'podcast-person-manage' => 'Gestion des intervenants',
'contributors' => 'Contributeurs',
'contributor-list' => 'Tous les contributeurs',
'contributor-add' => 'Ajouter un contributeur',

View File

@ -0,0 +1,20 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class CreditModel extends Model
{
protected $table = 'credits';
protected $allowedFields = [];
protected $returnType = \App\Entities\Credit::class;
}

View File

@ -89,6 +89,26 @@ class EpisodeModel extends Model
return $found;
}
public function getEpisodeById($podcastId, $episodeId)
{
if (!($found = cache("podcast{$podcastId}_episode{$episodeId}"))) {
$found = $this->where([
'podcast_id' => $podcastId,
'id' => $episodeId,
])
->where('published_at <=', 'NOW()')
->first();
cache()->save(
"podcast{$podcastId}_episode{$episodeId}",
$found,
DECADE
);
}
return $found;
}
/**
* Returns the previous episode based on episode ordering
*/
@ -334,7 +354,7 @@ class EpisodeModel extends Model
return $data;
}
protected function clearCache(array $data)
public function clearCache(array $data)
{
$episodeModel = new EpisodeModel();
$episode = (new EpisodeModel())->find(
@ -366,6 +386,7 @@ class EpisodeModel extends Model
cache()->delete(
"page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}"
);
cache()->delete("credits_{$locale}");
}
foreach ($years as $year) {

View File

@ -0,0 +1,150 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class EpisodePersonModel extends Model
{
protected $table = 'episodes_persons';
protected $primaryKey = 'id';
protected $allowedFields = [
'id',
'podcast_id',
'episode_id',
'person_id',
'person_group',
'person_role',
];
protected $returnType = \App\Entities\EpisodePerson::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
protected $validationRules = [
'episode_id' => 'required',
'person_id' => 'required',
];
protected $validationMessages = [];
protected $afterInsert = ['clearCache'];
protected $beforeDelete = ['clearCache'];
public function getPersonsByEpisodeId($podcastId, $episodeId)
{
if (
!($found = cache(
"podcast{$podcastId}_episodes{$episodeId}_persons"
))
) {
$found = $this->select('episodes_persons.*')
->where('episode_id', $episodeId)
->join(
'persons',
'person_id=persons.id'
)
->orderby('full_name')
->findAll();
cache()->save(
"podcast{$podcastId}_episodes{$episodeId}_persons",
$found,
DECADE
);
}
return $found;
}
/**
* Add persons to episode
*
* @param int podcastId
* @param int $episodeId
* @param array $persons
* @param array $groups_roles
*
* @return integer|false Number of rows inserted or FALSE on failure
*/
public function addEpisodePersons(
$podcastId,
$episodeId,
$persons,
$groups_roles
) {
if (!empty($persons)) {
$this->clearCache([
'id' => [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
],
]);
$data = [];
foreach ($persons as $person) {
if ($groups_roles) {
foreach ($groups_roles as $group_role) {
$group_role = explode(',', $group_role);
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $person,
'person_group' => $group_role[0],
'person_role' => $group_role[1],
];
}
} else {
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $person,
];
}
}
return $this->insertBatch($data);
}
return 0;
}
public function removeEpisodePersons(
$podcastId,
$episodeId,
$episodePersonId
) {
return $this->delete([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'id' => $episodePersonId,
]);
}
protected function clearCache(array $data)
{
$podcastId = null;
$episodeId = null;
if (
isset($data['id']['podcast_id']) &&
isset($data['id']['episode_id'])
) {
$podcastId = $data['id']['podcast_id'];
$episodeId = $data['id']['episode_id'];
} else {
$episodePerson = (new EpisodePersonModel())->find(
is_array($data['id']) ? $data['id']['id'] : $data['id']
);
$podcastId = $episodePerson->podcast_id;
$episodeId = $episodePerson->episode_id;
}
cache()->delete("podcast{$podcastId}_episodes{$episodeId}_persons");
(new EpisodeModel())->clearCache(['id' => $episodeId]);
return $data;
}
}

134
app/Models/PersonModel.php Normal file
View File

@ -0,0 +1,134 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class PersonModel extends Model
{
protected $table = 'persons';
protected $primaryKey = 'id';
protected $allowedFields = [
'id',
'full_name',
'unique_name',
'information_url',
'image_uri',
'created_by',
'updated_by',
];
protected $returnType = \App\Entities\Person::class;
protected $useSoftDeletes = false;
protected $useTimestamps = true;
protected $validationRules = [
'full_name' => 'required',
'unique_name' =>
'required|regex_match[/^[a-z0-9\-]{1,191}$/]|is_unique[persons.unique_name,id,{id}]',
'image_uri' => 'required',
'created_by' => 'required',
'updated_by' => 'required',
];
protected $validationMessages = [];
// clear cache before update if by any chance, the person name changes, so will the person link
protected $afterInsert = ['clearCache'];
protected $beforeUpdate = ['clearCache'];
protected $beforeDelete = ['clearCache'];
public function getPersonById($personId)
{
if (!($found = cache("person{$personId}"))) {
$found = $this->find($personId);
cache()->save("person{$personId}", $found, DECADE);
}
return $found;
}
public function getPerson($fullName)
{
return $this->where('full_name', $fullName)->first();
}
public function createPerson($fullName, $informationUrl, $image)
{
$person = new \App\Entities\Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $informationUrl,
'image' => download_file($image),
'created_by' => user()->id,
'updated_by' => user()->id,
]);
return $this->insert($person);
}
public function getPersonOptions()
{
$options = [];
if (!($options = cache('person_options'))) {
$options = array_reduce(
$this->select('`id`, `full_name`')
->orderBy('`full_name`', 'ASC')
->findAll(),
function ($result, $person) {
$result[$person->id] = $person->full_name;
return $result;
},
[]
);
cache()->save('person_options', $options, DECADE);
}
return $options;
}
public function getTaxonomyOptions()
{
$options = [];
$locale = service('request')->getLocale();
if (!($options = cache("taxonomy_options_{$locale}"))) {
foreach (lang('PersonsTaxonomy.persons') as $group_key => $group) {
foreach ($group['roles'] as $role_key => $role) {
$options[
"$group_key,$role_key"
] = "{$group['label']}{$role['label']}";
}
}
cache()->save("taxonomy_options_{$locale}", $options, DECADE);
}
return $options;
}
protected function clearCache(array $data)
{
$person = (new PersonModel())->getPersonById(
is_array($data['id']) ? $data['id'][0] : $data['id']
);
cache()->delete('person_options');
cache()->delete("person{$person->id}");
cache()->delete("user{$person->created_by}_persons");
$supportedLocales = config('App')->supportedLocales;
// clear cache for every credit page
foreach ($supportedLocales as $locale) {
cache()->delete("credit_{$locale}");
}
return $data;
}
}

View File

@ -16,14 +16,20 @@ use CodeIgniter\Model;
class PlatformModel extends Model
{
protected $table = 'platforms';
protected $primaryKey = 'id';
protected $primaryKey = 'slug';
protected $allowedFields = ['slug', 'label', 'home_url', 'submit_url'];
protected $allowedFields = [
'slug',
'type',
'label',
'home_url',
'submit_url',
];
protected $returnType = \App\Entities\Platform::class;
protected $useSoftDeletes = false;
protected $useTimestamps = true;
protected $useTimestamps = false;
public function getPlatforms()
{
@ -37,26 +43,32 @@ class PlatformModel extends Model
return $found;
}
public function getOrCreatePlatform($slug, $platformType)
public function getPlatform($slug)
{
if (!($found = cache("platforms_$slug"))) {
if (!($found = cache("platform_$slug"))) {
$found = $this->where('slug', $slug)->first();
if (!$found) {
$data = [
'slug' => $slug,
'type' => $platformType,
'label' => $slug,
'home_url' => '',
'submit_url' => null,
];
$this->insert($data);
$found = $this->where('slug', $slug)->first();
}
cache()->save("platforms_$slug", $found, DECADE);
cache()->save("platform_$slug", $found, DECADE);
}
return $found;
}
public function createPlatform(
$slug,
$type,
$label,
$homeUrl,
$submitUrl = null
) {
$data = [
'slug' => $slug,
'type' => $type,
'label' => $label,
'home_url' => $homeUrl,
'submit_url' => $submitUrl,
];
return $this->insert($data, false);
}
public function getPlatformsWithLinks($podcastId, $platformType)
{
if (

View File

@ -1,7 +1,7 @@
<?php
/**
* @copyright 2020 Podlibre
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -170,7 +170,7 @@ class PodcastModel extends Model
: false;
}
protected function clearCache(array $data)
public function clearCache(array $data)
{
$podcast = (new PodcastModel())->getPodcastById(
is_array($data['id']) ? $data['id'][0] : $data['id']
@ -195,6 +195,10 @@ class PodcastModel extends Model
);
}
}
// clear cache for every credit page
foreach ($supportedLocales as $locale) {
cache()->delete("credits_{$locale}");
}
// delete episode lists cache per year / season
// and localized pages

View File

@ -0,0 +1,119 @@
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class PodcastPersonModel extends Model
{
protected $table = 'podcasts_persons';
protected $primaryKey = 'id';
protected $allowedFields = [
'id',
'podcast_id',
'person_id',
'person_group',
'person_role',
];
protected $returnType = \App\Entities\PodcastPerson::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
protected $validationRules = [
'podcast_id' => 'required',
'person_id' => 'required',
];
protected $validationMessages = [];
protected $afterInsert = ['clearCache'];
protected $beforeDelete = ['clearCache'];
public function getPersonsByPodcastId($podcastId)
{
if (!($found = cache("podcast{$podcastId}_persons"))) {
$found = $this->select('podcasts_persons.*')
->where('podcast_id', $podcastId)
->join(
'persons',
'person_id=persons.id'
)
->orderby('full_name')
->findAll();
cache()->save("podcast{$podcastId}_persons", $found, DECADE);
}
return $found;
}
/**
* Add persons to podcast
*
* @param int $podcastId
* @param array $persons
* @param array $groups_roles
*
* @return integer Number of rows inserted or FALSE on failure
*/
public function addPodcastPersons($podcastId, $persons, $groups_roles)
{
if (!empty($persons)) {
$this->clearCache(['id' => ['podcast_id' => $podcastId]]);
$data = [];
foreach ($persons as $person) {
if ($groups_roles) {
foreach ($groups_roles as $group_role) {
$group_role = explode(',', $group_role);
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
'person_group' => $group_role[0],
'person_role' => $group_role[1],
];
}
} else {
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
];
}
}
return $this->insertBatch($data);
}
return 0;
}
public function removePodcastPersons($podcastId, $podcastPersonId)
{
return $this->delete([
'podcast_id' => $podcastId,
'id' => $podcastPersonId,
]);
}
protected function clearCache(array $data)
{
$podcastId = null;
if (isset($data['id']['podcast_id'])) {
$podcastId = $data['id']['podcast_id'];
} else {
$person = (new PodcastPersonModel())->find(
is_array($data['id']) ? $data['id']['id'] : $data['id']
);
$podcastId = $person->podcast_id;
}
cache()->delete("podcast{$podcastId}_persons");
(new PodcastModel())->clearCache(['id' => $podcastId]);
return $data;
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12.414 5H21a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7.414l2 2zM4 5v14h16V7h-8.414l-2-2H4zm4 13a4 4 0 1 1 8 0H8zm4-5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -0,0 +1,11 @@
<svg version="1.1" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<rect width="300" height="300" rx="67" fill="#f2f8ff"/>
<g transform="matrix(1.36 0 0 1.36 -45.282 22.882)">
<path d="m133.88 120.47c4.08 12.49 10.64 23.85 19.12 33.5 15.88-2.06 29.85-10.22 39.46-22.06-7.84 2.07-16.07 3.17-24.56 3.17-6.79 0-13.42-.72-19.81-2.06-6.67-1.36-12.07-6.21-14.21-12.55z" fill="#1269ff"/>
<path d="m145.09 154.47c2.68 0 5.32-.17 7.91-.51-8.48-9.65-15.04-21.01-19.12-33.5-.64-1.91-.99-3.95-.99-6.07 0-4.77 1.76-9.12 4.66-12.46-11.1 12.41-19.02 27.71-22.48 44.64 8.85 5.03 19.1 7.9 30.02 7.9z" fill="#5c9dff"/>
<path d="m85.78 107.8c4 16.61 14.79 30.57 29.28 38.78 3.47-16.95 11.4-32.28 22.52-44.69 3.48-3.98 8.6-6.49 14.3-6.49 1.27 0 2.5.12 3.7.36-6.36-1.33-12.95-2.04-19.71-2.04-18.35-.01-35.5 5.14-50.09 14.08z" fill="#9ec6ff"/>
<path d="m155.59 95.754c-6.36-1.33-12.95-2.04-19.71-2.04-18.36 0-35.51 5.15-50.1 14.09-1.1-4.59-1.69-9.39-1.69-14.33 0-19.02 8.71-36.01 22.35-47.2 29.91 9.03 53.64 32.31 63.39 61.91.09.26.18.52.26.79-.08-.26-.17-.53-.26-.79-2.19-6.29-7.6-11.12-14.24-12.43z" fill="#d1e3ff"/>
<path d="m203.08 82.474c0-27-22.49-50-58-50-14.66 0-28.12 5.18-38.64 13.8 30.18 9.12 54.07 32.7 63.64 62.69.56 1.75 1.07 3.62 1.53 5.42 17.34-.38 31.47-14.48 31.47-31.91z" fill="#fff"/>
<path d="m151.89 98.394c1.12 0 2.06.09 3.12.3 5.56 1.1 10.12 5.14 11.98 10.43.54 1.7 1.25 4.25 1.7 6l.59 2.31 2.38-.05c18.94-.4 34.43-15.81 34.43-34.91 0-28.93-24.13-53-61-53-15.21.05-29.63 5.58-40.55 14.48-14.31 11.73-23.45 29.56-23.45 49.52.01 5.12.63 10.27 1.78 15.03 4.2 17.43 15.52 32.08 30.71 40.68 9.19 5.18 20.18 8.25 31.51 8.28 2.78 0 5.61-.19 8.29-.53 16.67-2.16 31.32-10.73 41.41-23.14l.66-.81-1.28-4.63-2.47.65c-7.66 2.03-15.51 3.08-23.8 3.07-6.63 0-12.95-.68-19.19-2-5.62-1.13-10.19-5.22-11.99-10.56-.58-1.68-.84-3.24-.83-5.11-.06-4.17 1.37-7.6 3.95-10.53 2.94-3.36 7.25-5.48 12.05-5.48zm-6.8-62.92c34.12 0 55 21.92 55 47 0 14.99-11.56 27.3-26.23 28.78-.39-1.39-.81-2.86-1.17-3.98-9.29-29.1-31.41-51.63-59.87-62.03 9.25-6.28 20.16-9.81 32.27-9.77zm-58 58c0-17.51 7.76-33.21 20.03-43.85 23.34 7.48 42.47 23.45 53.64 44.64-1.45-.64-2.98-1.13-4.58-1.45-6.5-1.35-13.39-2.1-20.3-2.1-17.28.03-33.84 4.58-48 12.4-.53-3.13-.79-6.31-.79-9.64zm60.39 42.48c6.53 1.37 13.47 2.12 20.42 2.12 5.31 0 10.59-.44 15.72-1.25-8.1 7.21-18.23 12.17-29.43 13.94-4.26-5-7.96-10.4-11.05-16.17 1.38.59 2.83 1.05 4.34 1.36zm-.45 15.49c-.64.02-1.29.03-1.94.03-9.73.03-18.59-2.26-26.63-6.45 2.31-10.17 6.25-19.6 11.58-28.14.18 1.57.53 3.13.99 4.52 3.57 10.89 9.08 21.14 16 30.04zm-11.69-51.54c-10.42 11.68-18.29 26.2-22.27 41.94-11.43-7.58-20.01-19.1-23.82-32.62 13.79-8.04 29.5-12.54 46.63-12.51.96 0 1.91.01 2.86.04-1.24.93-2.38 1.98-3.4 3.15z" fill="#003dad"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -11,8 +11,8 @@
<link rel="stylesheet" href="/assets/index.css"/>
</head>
<body class="flex flex-col min-h-screen mx-auto">
<header class="border-b">
<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
<header class="bg-white border-b">
<div class="container flex items-center justify-between px-2 py-4 mx-auto">
<a href="<?= route_to('home') ?>" class="text-2xl"><?= isset($page)
? $page->title
@ -22,11 +22,15 @@
<main class="container flex-1 px-4 py-10 mx-auto">
<?= $this->renderSection('content') ?>
</main>
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t">
<?= render_page_links() ?>
<small><?= lang('Common.powered_by', [
'castopod' =>
'<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
]) ?></small>
</footer>
<footer class="px-2 py-4 bg-white border-t">
<div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row ">
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
<p class="flex flex-col items-center md:items-end">
<?= lang('Common.powered_by', [
'castopod' =>
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
]) ?>
</p>
</div>
</footer>
</body>

View File

@ -5,6 +5,10 @@ $navigation = [
'icon' => 'mic',
'items' => ['podcast-list', 'podcast-create', 'podcast-import'],
],
'persons' => [
'icon' => 'folder-user',
'items' => ['person-list', 'person-create'],
],
'users' => ['icon' => 'group', 'items' => ['user-list', 'user-create']],
'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']],
]; ?>

View File

@ -61,6 +61,11 @@
$podcast->id,
$episode->id
) ?>"><?= lang('Episode.edit') ?></a>
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
'episode-person-manage',
$podcast->id,
$episode->id
) ?>"><?= lang('Person.persons') ?></a>
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
'soundbites-edit',
$podcast->id,

View File

@ -0,0 +1,131 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.episode_form.title') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.episode_form.title') ?> (<?= count($episodePersons) ?>)
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?= button(
lang('Person.create'),
route_to('person-create'),
['variant' => 'primary', 'iconLeft' => 'add'],
['class' => 'mr-2']
) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open(route_to('episode-person-edit', $episode->id), [
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?php if ($episodePersons): ?>
<?= form_section(
lang('Person.episode_form.manage_section_title'),
lang('Person.episode_form.manage_section_subtitle')
) ?>
<?= data_table(
[
[
'header' => lang('Person.episode_form.person'),
'cell' => function ($episodePerson) {
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $episodePerson->person->id) .
"\"><img src=\"{$episodePerson->person->image->thumbnail_url}\" alt=\"{$episodePerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' .
$episodePerson->person->full_name .
($episodePerson->person_group && $episodePerson->person_role
? '<span class="text-sm text-gray-600">' .
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label"
) .
' ▸ ' .
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label"
) .
'</span>'
: '') .
(empty($episodePerson->person->information_url)
? ''
: "<a href=\"{$episodePerson->person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
$episodePerson->person->information_url .
'</a>') .
'</div></div>';
},
],
[
'header' => lang('Common.actions'),
'cell' => function ($episodePerson) {
return button(
lang('Person.episode_form.remove'),
route_to(
'episode-person-remove',
$episodePerson->podcast_id,
$episodePerson->episode_id,
$episodePerson->id
),
['variant' => 'danger', 'size' => 'small']
);
},
],
],
$episodePersons
) ?>
<?= form_section_close() ?>
<?php endif; ?>
<?= form_section(
lang('Person.episode_form.add_section_title'),
lang('Person.episode_form.add_section_subtitle')
) ?>
<?= form_label(
lang('Person.episode_form.person'),
'person',
[],
lang('Person.episode_form.person_hint')
) ?>
<?= form_multiselect('person[]', $personOptions, old('person', []), [
'id' => 'person',
'class' => 'form-select mb-4',
'required' => 'required',
]) ?>
<?= form_label(
lang('Person.episode_form.group_role'),
'group_role',
[],
lang('Person.episode_form.group_role_hint'),
true
) ?>
<?= form_multiselect(
'person_group_role[]',
$taxonomyOptions,
old('person_group_role', []),
['id' => 'person_group_role', 'class' => 'form-select mb-4']
) ?>
<?= form_section_close() ?>
<?= button(
lang('Person.episode_form.submit_add'),
null,
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end']
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -64,6 +64,12 @@
['variant' => 'info', 'iconLeft' => 'edit'],
['class' => 'mb-4']
) ?>
<?= button(
lang('Person.episode_form.title'),
route_to('episode-person-manage', $podcast->id, $episode->id),
['variant' => 'info', 'iconLeft' => 'folder-user'],
['class' => 'mb-4']
) ?>
<?php if (count($episode->soundbites) > 0): ?>
<?= data_table(
[

View File

@ -0,0 +1,95 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.create') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open_multipart(route_to('person-create'), [
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?= form_section(
lang('Person.form.identity_section_title'),
lang('Person.form.identity_section_subtitle')
) ?>
<?= form_label(
lang('Person.form.full_name'),
'full_name',
[],
lang('Person.form.full_name_hint')
) ?>
<?= form_input([
'id' => 'full_name',
'name' => 'full_name',
'class' => 'form-input mb-4',
'value' => old('full_name'),
'required' => 'required',
'data-slugify' => 'title',
]) ?>
<?= form_label(
lang('Person.form.unique_name'),
'unique_name',
[],
lang('Person.form.unique_name_hint')
) ?>
<?= form_input([
'id' => 'unique_name',
'name' => 'unique_name',
'class' => 'form-input mb-4',
'value' => old('unique_name'),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
<?= form_label(
lang('Person.form.information_url'),
'information_url',
[],
lang('Person.form.information_url_hint'),
true
) ?>
<?= form_input([
'id' => 'information_url',
'name' => 'information_url',
'class' => 'form-input mb-4',
'value' => old('information_url'),
]) ?>
<?= form_label(lang('Person.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'required' => 'required',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<small class="mb-4 text-gray-600"><?= lang(
'Person.form.image_size_hint'
) ?></small>
<?= form_section_close() ?>
<?= button(
lang('Person.form.submit_create'),
null,
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end']
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,95 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open_multipart(route_to('person-edit', $person->id), [
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?= form_section(
lang('Person.form.identity_section_title'),
lang('Person.form.identity_section_subtitle') .
"<img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-32 h-32 mt-3 rounded\" />"
) ?>
<?= form_label(
lang('Person.form.full_name'),
'full_name',
[],
lang('Person.form.full_name_hint')
) ?>
<?= form_input([
'id' => 'full_name',
'name' => 'full_name',
'class' => 'form-input mb-4',
'value' => old('full_name', $person->full_name),
'required' => 'required',
'data-slugify' => 'title',
]) ?>
<?= form_label(
lang('Person.form.unique_name'),
'unique_name',
[],
lang('Person.form.unique_name_hint')
) ?>
<?= form_input([
'id' => 'unique_name',
'name' => 'unique_name',
'class' => 'form-input mb-4',
'value' => old('unique_name', $person->unique_name),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
<?= form_label(
lang('Person.form.information_url'),
'information_url',
[],
lang('Person.form.information_url_hint'),
true
) ?>
<?= form_input([
'id' => 'information_url',
'name' => 'information_url',
'class' => 'form-input mb-4',
'value' => old('information_url', $person->information_url),
]) ?>
<?= form_label(lang('Person.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<small class="mb-4 text-gray-600"><?= lang(
'Person.form.image_size_hint'
) ?></small>
<?= form_section_close() ?>
<?= button(
lang('Person.form.submit_edit'),
null,
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end']
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,65 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.all_persons') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.all_persons') ?> (<?= count($persons) ?>)
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?= button(
lang('Person.create'),
route_to('person-create'),
['variant' => 'primary', 'iconLeft' => 'add'],
['class' => 'mr-2']
) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="flex flex-wrap">
<?php if (!empty($persons)): ?>
<?php foreach ($persons as $person): ?>
<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
<img
alt="<?= $person->full_name ?>"
src="<?= $person->image
->thumbnail_url ?>" class="object-cover w-40 w-full" />
<div class="p-2">
<a href="<?= route_to(
'person-view',
$person->id
) ?>" class="hover:underline">
<h2 class="font-semibold"><?= $person->full_name ?></h2>
</a>
</div>
<footer class="flex items-center justify-end p-2">
<a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
'person-edit',
$person->id
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
'Person.edit'
) ?>"><?= icon('edit') ?></a>
<a class="inline-flex p-2 mr-2 text-gray-700 bg-red-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
'person-delete',
$person->id
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
'Person.delete'
) ?>"><?= icon('delete-bin') ?></a>
<a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
'person-view',
$person->id
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
'Person.view'
) ?>"><?= icon('eye') ?></a>
</footer>
</article>
<?php endforeach; ?>
<?php else: ?>
<p class="italic"><?= lang('Person.no_person') ?></p>
<?php endif; ?>
</div>
<?= $this->endSection() ?>

View File

@ -0,0 +1,38 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= $person->full_name ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= $person->full_name ?>
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?= button(
lang('Person.edit'),
route_to('person-edit', $person->id),
['variant' => 'secondary', 'iconLeft' => 'edit'],
['class' => 'mr-2']
) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="flex flex-wrap">
<div class="w-full max-w-sm mb-6 md:mr-4">
<img
src="<?= $person->image->medium_url ?>"
alt="$person->full_name"
class="object-cover w-full rounded"
/>
</div>
<section class="w-full prose">
<?= $person->full_name ?><br />
<a href="<?= $person->information_url ?>"><?= $person->information_url ?></a>
</section>
</div>
<?= $this->endSection() ?>

View File

@ -8,6 +8,10 @@ $podcastNavigation = [
'icon' => 'mic',
'items' => ['episode-list', 'episode-create'],
],
'persons' => [
'icon' => 'folder-user',
'items' => ['podcast-person-manage'],
],
'analytics' => [
'icon' => 'line-chart',
'items' => [

View File

@ -58,6 +58,16 @@
$podcast->id,
$episode->id
) ?>"><?= lang('Episode.edit') ?></a>
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
'episode-person-manage',
$podcast->id,
$episode->id
) ?>"><?= lang('Person.persons') ?></a>
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
'soundbites-edit',
$podcast->id,
$episode->id
) ?>"><?= lang('Episode.soundbites') ?></a>
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
'episode',
$podcast->name,

View File

@ -0,0 +1,131 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.podcast_form.title') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.podcast_form.title') ?> (<?= count($podcastPersons) ?>)
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?= button(
lang('Person.create'),
route_to('person-create'),
['variant' => 'primary', 'iconLeft' => 'add'],
['class' => 'mr-2']
) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open(route_to('podcast-person-edit', $podcast->id), [
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?php if ($podcastPersons): ?>
<?= form_section(
lang('Person.podcast_form.manage_section_title'),
lang('Person.podcast_form.manage_section_subtitle')
) ?>
<?= data_table(
[
[
'header' => lang('Person.podcast_form.person'),
'cell' => function ($podcastPerson) {
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $podcastPerson->person->id) .
"\"><img src=\"{$podcastPerson->person->image->thumbnail_url}\" alt=\"{$podcastPerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' .
$podcastPerson->person->full_name .
($podcastPerson->person_group && $podcastPerson->person_role
? '<span class="text-sm text-gray-600">' .
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label"
) .
' ▸ ' .
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label"
) .
'</span>'
: '') .
(empty($podcastPerson->person->information_url)
? ''
: "<a href=\"{$podcastPerson->person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
$podcastPerson->person->information_url .
'</a>') .
'</div></div>';
},
],
[
'header' => lang('Common.actions'),
'cell' => function ($podcastPerson) {
return button(
lang('Person.podcast_form.remove'),
route_to(
'podcast-person-remove',
$podcastPerson->podcast_id,
$podcastPerson->id
),
['variant' => 'danger', 'size' => 'small']
);
},
],
],
$podcastPersons
) ?>
<?= form_section_close() ?>
<?php endif; ?>
<?= form_section(
lang('Person.podcast_form.add_section_title'),
lang('Person.podcast_form.add_section_subtitle')
) ?>
<?= form_label(
lang('Person.podcast_form.person'),
'person',
[],
lang('Person.podcast_form.person_hint')
) ?>
<?= form_multiselect('person[]', $personOptions, old('person', []), [
'id' => 'person',
'class' => 'form-select mb-4',
'required' => 'required',
]) ?>
<?= form_label(
lang('Person.podcast_form.group_role'),
'group_role',
[],
lang('Person.podcast_form.group_role_hint'),
true
) ?>
<?= form_multiselect(
'person_group_role[]',
$taxonomyOptions,
old('person_group_role', []),
['id' => 'person_group_role', 'class' => 'form-select mb-4']
) ?>
<?= form_section_close() ?>
<?= button(
lang('Person.podcast_form.submit_add'),
null,
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end']
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

49
app/Views/credits.php Normal file
View File

@ -0,0 +1,49 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.credits') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<?php foreach ($credits as $groupSlug => $groups): ?>
<?php if (
$groupSlug
): ?><div class="col-span-1 mt-12 mb-2 text-xl font-bold text-gray-500 md:text-2xl md:col-span-2 "><?= $groups[
'group_label'
] ?></div><?php endif; ?>
<?php foreach ($groups['persons'] as $personId => $persons): ?>
<div class="flex mt-2 mb-2">
<img src="<?= $persons['thumbnail_url'] ?>" alt="<?= $persons[
'full_name'
] ?>" class="object-cover w-16 h-16 border-4 rounded-full md:h-24 md:w-24 border-gray" />
<div class="flex flex-col ml-3 mr-4"><span class="text-lg font-bold text-gray-700 md:text-xl"><?= $persons[
'full_name'
] ?></span>
<?php if (
!empty($persons['information_url'])
): ?><a href="<?= $persons[
'information_url'
] ?>" class="text-sm text-blue-800 hover:underline" target="_blank" rel="noreferrer noopener"><?= $persons[
'information_url'
] ?></a><?php endif; ?></div>
</div>
<div class="flex flex-col">
<?php foreach ($persons['roles'] as $role_slug => $role_array): ?>
<?= $role_array['role_label'] ?>
<?php foreach ($role_array['is_in'] as $isIn): ?>
<a href="<?= $isIn[
'link'
] ?>" class="text-sm text-gray-500 hover:underline"><?= $isIn[
'title'
] ?></a>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
<?php $this->endSection(); ?>

View File

@ -100,11 +100,28 @@
<?= format_duration($episode->enclosure_duration) ?>
</time>
</div>
<div class="flex mt-2 mb-1 space-x-2">
<?php foreach ($persons as $person): ?>
<?php if (!empty($person['information_url'])): ?>
<a href="<?= $person[
'information_url'
] ?>" target="_blank" rel="noreferrer noopener">
<?php endif; ?>
<img src="<?= $person['thumbnail_url'] ?>" alt="<?= $person[
'full_name'
] ?>" title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" class="object-cover w-12 h-12 rounded-full" />
<?php if (!empty($person['information_url'])): ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?= location_link(
$episode->location_name,
$episode->location_geo,
$episode->location_osmid,
'self-start mt-2'
'self-start mt-2 mb-2'
) ?>
<audio controls preload="none" class="w-full mt-auto">
<source src="<?= $episode->enclosure_web_url ?>" type="<?= $episode->enclosure_type ?>">

View File

@ -114,6 +114,26 @@
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="flex mb-2 space-x-2">
<?php foreach ($personArray as $person): ?>
<?php if (!empty($person['information_url'])): ?>
<a href="<?= $person[
'information_url'
] ?>" target="_blank" rel="noreferrer noopener">
<?php endif; ?>
<img src="<?= $person[
'thumbnail_url'
] ?>" alt="<?= $person[
'full_name'
] ?>" title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" class="object-cover w-12 h-12 rounded-full" />
<?php if (!empty($person['information_url'])): ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="mb-2 opacity-75">
<?= $podcast->description_html ?>

View File

@ -16,7 +16,8 @@
"vlucas/phpdotenv": "^5.2",
"league/html-to-markdown": "^4.10",
"opawg/user-agents-php": "^1.0",
"podlibre/ipcat": "^1.0"
"podlibre/ipcat": "^1.0",
"podlibre/podcast-namespace": "^1.0.6"
},
"require-dev": {
"mikey179/vfsstream": "1.6.*",
@ -33,13 +34,19 @@
"post-install-cmd": [
"@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php > vendor/opawg/user-agents-php/src/UserAgents.php",
"@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php > vendor/opawg/user-agents-php/src/UserAgentsRSS.php",
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php"
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php",
"@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json > app/Language/en/PersonsTaxonomy.php",
"@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-fr.json > app/Language/fr/PersonsTaxonomy.php",
"@php vendor/podlibre/podcast-namespace/src/ReversedTaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json > vendor/podlibre/podcast-namespace/src/ReversedTaxonomy.php"
],
"post-update-cmd": [
"@composer dump-autoload",
"@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php > vendor/opawg/user-agents-php/src/UserAgents.php",
"@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php > vendor/opawg/user-agents-php/src/UserAgentsRSS.php",
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php"
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php",
"@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json > app/Language/en/PersonsTaxonomy.php",
"@php vendor/podlibre/podcast-namespace/src/TaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-fr.json > app/Language/fr/PersonsTaxonomy.php",
"@php vendor/podlibre/podcast-namespace/src/ReversedTaxonomyGenerate.php https://raw.githubusercontent.com/Podcastindex-org/podcast-namespace/main/taxonomy-en.json > vendor/podlibre/podcast-namespace/src/ReversedTaxonomy.php"
]
},
"support": {

29
composer.lock generated
View File

@ -1071,6 +1071,35 @@
},
"time": "2020-10-05T17:15:07+00:00"
},
{
"name": "podlibre/podcast-namespace",
"version": "v1.0.6",
"source": {
"type": "git",
"url": "https://code.podlibre.org/podlibre/podcastnamespace",
"reference": "4525c06ee9dd95bb745ee875d55b64a053c74cd6"
},
"type": "library",
"autoload": {
"psr-4": {
"Podlibre\\PodcastNamespace\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Benjamin Bellamy",
"email": "ben@podlibre.org",
"homepage": "https://podlibre.org/"
}
],
"description": "PHP implementation for the Podcast Namespace.",
"homepage": "https://code.podlibre.org/podlibre/podcastnamespace",
"time": "2021-01-14T15:47:06+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",