refactor(persons): move podcast and episode persons models to person model for consistency

- fix lazy loading properties + podcast import controller
- rename all snake_case variables to
camelCase
- fix broken routes
- refactor Location construction logic and setters
This commit is contained in:
Yassine Doghri 2021-05-17 17:11:23 +00:00
parent 6b74a9e98a
commit 93e605b406
No known key found for this signature in database
GPG Key ID: 3E7F89498B960C9F
48 changed files with 944 additions and 926 deletions

View File

@ -164,13 +164,13 @@ $routes->group(
]); ]);
$routes->group('persons', function ($routes): void { $routes->group('persons', function ($routes): void {
$routes->get('/', 'PodcastPodcastController/$1', [ $routes->get('/', 'PodcastPersonController/$1', [
'as' => 'podcast-person-manage', 'as' => 'podcast-person-manage',
'filter' => 'permission:podcast-edit', 'filter' => 'permission:podcast-edit',
]); ]);
$routes->post( $routes->post(
'/', '/',
'PodcastPodcastController::attemptAdd/$1', 'PodcastPersonController::attemptAdd/$1',
[ [
'filter' => 'permission:podcast-edit', 'filter' => 'permission:podcast-edit',
], ],
@ -178,7 +178,7 @@ $routes->group(
$routes->get( $routes->get(
'(:num)/remove', '(:num)/remove',
'PodcastPodcastController::remove/$1/$2', 'PodcastPersonController::remove/$1/$2',
[ [
'as' => 'podcast-person-remove', 'as' => 'podcast-person-remove',
'filter' => 'permission:podcast-edit', 'filter' => 'permission:podcast-edit',

View File

@ -12,6 +12,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use Config\Database; use Config\Database;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Location;
use App\Entities\Note; use App\Entities\Note;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
@ -133,7 +134,9 @@ class EpisodeController extends BaseController
'audio_file' => $this->request->getFile('audio_file'), 'audio_file' => $this->request->getFile('audio_file'),
'description_markdown' => $this->request->getPost('description'), 'description_markdown' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'), 'image' => $this->request->getFile('image'),
'location' => $this->request->getPost('location_name'), 'location' => new Location(
$this->request->getPost('location_name'),
),
'transcript' => $this->request->getFile('transcript'), 'transcript' => $this->request->getFile('transcript'),
'chapters' => $this->request->getFile('chapters'), 'chapters' => $this->request->getFile('chapters'),
'parental_advisory' => 'parental_advisory' =>
@ -249,7 +252,9 @@ class EpisodeController extends BaseController
$this->episode->description_markdown = $this->request->getPost( $this->episode->description_markdown = $this->request->getPost(
'description', 'description',
); );
$this->episode->location = $this->request->getPost('location_name'); $this->episode->location = new Location(
$this->request->getPost('location_name'),
);
$this->episode->parental_advisory = $this->episode->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined' $this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory') ? $this->request->getPost('parental_advisory')
@ -673,17 +678,17 @@ class EpisodeController extends BaseController
public function soundbitesAttemptEdit(): RedirectResponse public function soundbitesAttemptEdit(): RedirectResponse
{ {
$soundbites_array = $this->request->getPost('soundbites_array'); $soundbites = $this->request->getPost('soundbites');
$rules = [ $rules = [
'soundbites_array.0.start_time' => 'soundbites.0.start_time' =>
'permit_empty|required_with[soundbites_array.0.duration]|decimal|greater_than_equal_to[0]', 'permit_empty|required_with[soundbites.0.duration]|decimal|greater_than_equal_to[0]',
'soundbites_array.0.duration' => 'soundbites.0.duration' =>
'permit_empty|required_with[soundbites_array.0.start_time]|decimal|greater_than_equal_to[0]', 'permit_empty|required_with[soundbites.0.start_time]|decimal|greater_than_equal_to[0]',
]; ];
foreach (array_keys($soundbites_array) as $soundbite_id) { foreach (array_keys($soundbites) as $soundbite_id) {
$rules += [ $rules += [
"soundbites_array.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]', "soundbites.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
"soundbites_array.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]', "soundbites.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]',
]; ];
} }
if (!$this->validate($rules)) { if (!$this->validate($rules)) {
@ -693,16 +698,13 @@ class EpisodeController extends BaseController
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
foreach ($soundbites_array as $soundbite_id => $soundbite) { foreach ($soundbites as $soundbite_id => $soundbite) {
if ( if ((int) $soundbite['start_time'] < (int) $soundbite['duration']) {
$soundbite['start_time'] !== null &&
$soundbite['duration'] !== null
) {
$data = [ $data = [
'podcast_id' => $this->podcast->id, 'podcast_id' => $this->podcast->id,
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
'start_time' => $soundbite['start_time'], 'start_time' => (int) $soundbite['start_time'],
'duration' => $soundbite['duration'], 'duration' => (int) $soundbite['duration'],
'label' => $soundbite['label'], 'label' => $soundbite['label'],
'updated_by' => user_id(), 'updated_by' => user_id(),
]; ];
@ -711,6 +713,7 @@ class EpisodeController extends BaseController
} else { } else {
$data += ['id' => $soundbite_id]; $data += ['id' => $soundbite_id];
} }
$soundbiteModel = new SoundbiteModel(); $soundbiteModel = new SoundbiteModel();
if (!$soundbiteModel->save($data)) { if (!$soundbiteModel->save($data)) {
return redirect() return redirect()

View File

@ -23,7 +23,7 @@ class EpisodePersonController extends BaseController
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
if (count($params) <= 2) { if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
@ -54,10 +54,6 @@ class EpisodePersonController extends BaseController
$data = [ $data = [
'episode' => $this->episode, 'episode' => $this->episode,
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episodePersons' => (new PersonModel())->getEpisodePersons(
$this->podcast->id,
$this->episode->id,
),
'personOptions' => (new PersonModel())->getPersonOptions(), 'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(), 'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
]; ];
@ -65,7 +61,7 @@ class EpisodePersonController extends BaseController
0 => $this->podcast->title, 0 => $this->podcast->title,
1 => $this->episode->title, 1 => $this->episode->title,
]); ]);
return view('admin/episode/person', $data); return view('admin/episode/persons', $data);
} }
public function attemptAdd(): RedirectResponse public function attemptAdd(): RedirectResponse
@ -91,12 +87,12 @@ class EpisodePersonController extends BaseController
return redirect()->back(); return redirect()->back();
} }
public function remove(int $episodePersonId): RedirectResponse public function remove(int $personId): RedirectResponse
{ {
(new PersonModel())->removeEpisodePersons( (new PersonModel())->removePersonFromEpisode(
$this->podcast->id, $this->podcast->id,
$this->episode->id, $this->episode->id,
$episodePersonId, $personId,
); );
return redirect()->back(); return redirect()->back();

View File

@ -8,6 +8,7 @@
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\Image;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use App\Entities\Person; use App\Entities\Person;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
@ -77,7 +78,7 @@ class PersonController extends BaseController
'full_name' => $this->request->getPost('full_name'), 'full_name' => $this->request->getPost('full_name'),
'unique_name' => $this->request->getPost('unique_name'), 'unique_name' => $this->request->getPost('unique_name'),
'information_url' => $this->request->getPost('information_url'), 'information_url' => $this->request->getPost('information_url'),
'image' => $this->request->getFile('image'), 'image' => new Image($this->request->getFile('image')),
'created_by' => user_id(), 'created_by' => user_id(),
'updated_by' => user_id(), 'updated_by' => user_id(),
]); ]);
@ -125,9 +126,9 @@ class PersonController extends BaseController
$this->person->information_url = $this->request->getPost( $this->person->information_url = $this->request->getPost(
'information_url', 'information_url',
); );
$image = $this->request->getFile('image'); $imageFile = $this->request->getFile('image');
if ($image->isValid()) { if ($imageFile !== null && $imageFile->isValid()) {
$this->person->image = $image; $this->person->image = new Image($imageFile);
} }
$this->person->updated_by = user_id(); $this->person->updated_by = user_id();

View File

@ -9,6 +9,7 @@
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\Image; use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast; use App\Entities\Podcast;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use Config\Database; use Config\Database;
@ -170,7 +171,9 @@ class PodcastController extends BaseController
'publisher' => $this->request->getPost('publisher'), 'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'), 'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'), 'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name'), 'location' => new Location(
$this->request->getPost('location_name'),
),
'payment_pointer' => $this->request->getPost('payment_pointer'), 'payment_pointer' => $this->request->getPost('payment_pointer'),
'custom_rss_string' => $this->request->getPost('custom_rss'), 'custom_rss_string' => $this->request->getPost('custom_rss'),
'partner_id' => $this->request->getPost('partner_id'), 'partner_id' => $this->request->getPost('partner_id'),
@ -271,7 +274,9 @@ class PodcastController extends BaseController
$this->podcast->owner_email = $this->request->getPost('owner_email'); $this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type'); $this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright'); $this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name'); $this->podcast->location = new Location(
$this->request->getPost('location_name'),
);
$this->podcast->payment_pointer = $this->request->getPost( $this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer', 'payment_pointer',
); );

View File

@ -16,6 +16,7 @@ use Config\Database;
use Podlibre\PodcastNamespace\ReversedTaxonomy; use Podlibre\PodcastNamespace\ReversedTaxonomy;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Image; use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Person; use App\Entities\Person;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\LanguageModel; use App\Models\LanguageModel;
@ -121,7 +122,7 @@ class PodcastImportController extends BaseController
try { try {
if ( if (
$nsItunes->image !== null && isset($nsItunes->image) &&
$nsItunes->image->attributes()['href'] !== null $nsItunes->image->attributes()['href'] !== null
) { ) {
$imageFile = download_file( $imageFile = download_file(
@ -133,6 +134,15 @@ class PodcastImportController extends BaseController
); );
} }
$location = null;
if (isset($nsPodcast->location)) {
$location = new Location(
(string) $nsPodcast->location,
(string) $nsPodcast->location->attributes()['geo'],
(string) $nsPodcast->location->attributes()['osm'],
);
}
$podcast = new Podcast([ $podcast = new Podcast([
'name' => $this->request->getPost('name'), 'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost( 'imported_feed_url' => $this->request->getPost(
@ -150,40 +160,27 @@ class PodcastImportController extends BaseController
'language_code' => $this->request->getPost('language'), 'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'), 'category_id' => $this->request->getPost('category'),
'parental_advisory' => 'parental_advisory' =>
$nsItunes->explicit === null isset($nsItunes->explicit)
? null ? (in_array((string) $nsItunes->explicit, ['yes', 'true'])
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit' ? 'explicit'
: (in_array($nsItunes->explicit, ['no', 'false']) : (in_array((string) $nsItunes->explicit, ['no', 'false'])
? 'clean' ? 'clean'
: null)), : null))
: null,
'owner_name' => (string) $nsItunes->owner->name, 'owner_name' => (string) $nsItunes->owner->name,
'owner_email' => (string) $nsItunes->owner->email, 'owner_email' => (string) $nsItunes->owner->email,
'publisher' => (string) $nsItunes->author, 'publisher' => (string) $nsItunes->author,
'type' => 'type' => isset($nsItunes->type) ? (string) $nsItunes->type : 'episodic',
$nsItunes->type === null ? 'episodic' : $nsItunes->type,
'copyright' => (string) $feed->channel[0]->copyright, 'copyright' => (string) $feed->channel[0]->copyright,
'is_blocked' => 'is_blocked' =>
$nsItunes->block === null isset($nsItunes->block)
? false ? (string) $nsItunes->block === 'yes'
: $nsItunes->block === 'yes', : false,
'is_completed' => 'is_completed' =>
$nsItunes->complete === null isset($nsItunes->complete)
? false ? (string) $nsItunes->complete === 'yes'
: $nsItunes->complete === 'yes', : false,
'location_name' => $nsPodcast->location 'location' => $location,
? (string) $nsPodcast->location
: null,
'location_geo' =>
!$nsPodcast->location ||
$nsPodcast->location->attributes()['geo'] === null
? null
: (string) $nsPodcast->location->attributes()['geo'],
'location_osm_id' =>
!$nsPodcast->location ||
$nsPodcast->location->attributes()['osm'] === null
? null
: (string) $nsPodcast->location->attributes()['osm'],
'created_by' => user_id(), 'created_by' => user_id(),
'updated_by' => user_id(), 'updated_by' => user_id(),
]); ]);
@ -277,18 +274,17 @@ class PodcastImportController extends BaseController
} }
} }
$personGroup = // TODO: these checks should be in the taxonomy as default values
isset($podcastPerson->attributes()['group']) $podcastPersonGroup = $podcastPerson->attributes()['group'] ?? "Cast";
? ['slug' => ''] $podcastPersonRole = $podcastPerson->attributes()['role'] ?? "Host";
: ReversedTaxonomy::$taxonomy[(string) $podcastPerson->attributes()['group']];
$personRole = $personGroup = ReversedTaxonomy::$taxonomy[(string) $podcastPersonGroup];
isset($podcastPerson->attributes()['role']) ||
$personGroup === null $personGroupSlug = $personGroup['slug'];
? ['slug' => ''] $personRoleSlug = $personGroup['roles'][(string) $podcastPersonRole]['slug'];
: $personGroup['roles'][strval($podcastPerson->attributes()['role'])];
$podcastPersonModel = new PersonModel(); $podcastPersonModel = new PersonModel();
if (!$podcastPersonModel->addPodcastPerson($newPodcastId, $newPersonId, $personGroup['slug'], $personRole['slug'])) { if (!$podcastPersonModel->addPodcastPerson($newPodcastId, $newPersonId, $personGroupSlug, $personRoleSlug)) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
@ -341,7 +337,7 @@ class PodcastImportController extends BaseController
}; };
if ( if (
$nsItunes->image !== null && isset($nsItunes->image) &&
$nsItunes->image->attributes()['href'] !== null $nsItunes->image->attributes()['href'] !== null
) { ) {
$episodeImage = new Image( $episodeImage = new Image(
@ -353,6 +349,15 @@ class PodcastImportController extends BaseController
$episodeImage = null; $episodeImage = null;
} }
$location = null;
if (isset($nsPodcast->location)) {
$location = new Location(
(string) $nsPodcast->location,
(string) $nsPodcast->location->attributes()['geo'],
(string) $nsPodcast->location->attributes()['osm'],
);
}
$newEpisode = new Episode([ $newEpisode = new Episode([
'podcast_id' => $newPodcastId, 'podcast_id' => $newPodcastId,
'guid' => $item->guid ?? null, 'guid' => $item->guid ?? null,
@ -367,13 +372,13 @@ class PodcastImportController extends BaseController
'description_html' => $itemDescriptionHtml, 'description_html' => $itemDescriptionHtml,
'image' => $episodeImage, 'image' => $episodeImage,
'parental_advisory' => 'parental_advisory' =>
$nsItunes->explicit === null isset($nsItunes->explicit)
? null ? (in_array((string) $nsItunes->explicit, ['yes', 'true'])
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit' ? 'explicit'
: (in_array($nsItunes->explicit, ['no', 'false']) : (in_array((string) $nsItunes->explicit, ['no', 'false'])
? 'clean' ? 'clean'
: null)), : null))
: null,
'number' => 'number' =>
$this->request->getPost('force_renumber') === 'yes' $this->request->getPost('force_renumber') === 'yes'
? $itemNumber ? $itemNumber
@ -382,27 +387,13 @@ class PodcastImportController extends BaseController
$this->request->getPost('season_number') === null $this->request->getPost('season_number') === null
? $nsItunes->season ? $nsItunes->season
: $this->request->getPost('season_number'), : $this->request->getPost('season_number'),
'type' => 'type' => isset($nsItunes->episodeType)
$nsItunes->episodeType === null ? (string) $nsItunes->episodeType
? 'full' : 'full',
: $nsItunes->episodeType, 'is_blocked' => isset($nsItunes->block)
'is_blocked' => ? (string) $nsItunes->block === 'yes'
$nsItunes->block === null : false,
? false 'location' => $location,
: $nsItunes->block === 'yes',
'location_name' => $nsPodcast->location
? $nsPodcast->location
: null,
'location_geo' =>
!$nsPodcast->location ||
$nsPodcast->location->attributes()['geo'] === null
? null
: $nsPodcast->location->attributes()['geo'],
'location_osm_id' =>
!$nsPodcast->location ||
$nsPodcast->location->attributes()['osm'] === null
? null
: $nsPodcast->location->attributes()['osm'],
'created_by' => user_id(), 'created_by' => user_id(),
'updated_by' => user_id(), 'updated_by' => user_id(),
'published_at' => strtotime($item->pubDate), 'published_at' => strtotime($item->pubDate),
@ -425,14 +416,16 @@ class PodcastImportController extends BaseController
if (($newPerson = $personModel->getPerson($fullName)) !== null) { if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id; $newPersonId = $newPerson->id;
} else { } else {
$newEpisodePerson = new Person([ $newPerson = new Person([
'full_name' => $fullName, 'full_name' => $fullName,
'slug' => slugify($fullName), 'unique_name' => slugify($fullName),
'information_url' => $episodePerson->attributes()['href'], 'information_url' => $episodePerson->attributes()['href'],
'image' => new Image(download_file($episodePerson->attributes()['img'])) 'image' => new Image(download_file($episodePerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]); ]);
if (!($newPersonId = $personModel->insert($newEpisodePerson))) { if (!($newPersonId = $personModel->insert($newPerson))) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
@ -440,19 +433,17 @@ class PodcastImportController extends BaseController
} }
} }
$personGroup = // TODO: these checks should be in the taxonomy as default values
$episodePerson->attributes()['group'] === null $episodePersonGroup = $episodePerson->attributes()['group'] ?? "Cast";
? ['slug' => ''] $episodePersonRole = $episodePerson->attributes()['role'] ?? "Host";
: ReversedTaxonomy::$taxonomy[strval($episodePerson->attributes()['group'])];
$personRole =
$episodePerson->attributes()['role'] === null ||
$personGroup === null
? ['slug' => '']
: $personGroup['roles'][strval($episodePerson->attributes()['role'])];
$personGroup = ReversedTaxonomy::$taxonomy[(string) $episodePersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $episodePersonRole]['slug'];
$episodePersonModel = new PersonModel(); $episodePersonModel = new PersonModel();
if (!$episodePersonModel->addEpisodePerson($newPodcastId, $newEpisodeId, $newPersonId, $personGroup['slug'], $personRole['slug'])) { if (!$episodePersonModel->addEpisodePerson($newPodcastId, $newEpisodeId, $newPersonId, $personGroupSlug, $personRoleSlug)) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()

View File

@ -54,13 +54,13 @@ class PodcastPersonController extends BaseController
replace_breadcrumb_params([ replace_breadcrumb_params([
0 => $this->podcast->title, 0 => $this->podcast->title,
]); ]);
return view('admin/podcast/person', $data); return view('admin/podcast/persons', $data);
} }
public function attemptAdd(): RedirectResponse public function attemptAdd(): RedirectResponse
{ {
$rules = [ $rules = [
'person' => 'required', 'persons' => 'required',
]; ];
if (!$this->validate($rules)) { if (!$this->validate($rules)) {
@ -72,18 +72,18 @@ class PodcastPersonController extends BaseController
(new PersonModel())->addPodcastPersons( (new PersonModel())->addPodcastPersons(
$this->podcast->id, $this->podcast->id,
$this->request->getPost('person'), $this->request->getPost('persons'),
$this->request->getPost('person_group_role'), $this->request->getPost('roles') ?? [],
); );
return redirect()->back(); return redirect()->back();
} }
public function remove(int $podcastPersonId): RedirectResponse public function remove(int $personId): RedirectResponse
{ {
(new PersonModel())->removePodcastPersons( (new PersonModel())->removePersonFromPodcast(
$this->podcast->id, $this->podcast->id,
$podcastPersonId, $personId,
); );
return redirect()->back(); return redirect()->back();

View File

@ -69,26 +69,26 @@ class PageController extends BaseController
$allCredits = (new CreditModel())->findAll(); $allCredits = (new CreditModel())->findAll();
// Unlike the carpenter, we make a tree from a table: // Unlike the carpenter, we make a tree from a table:
$person_group = null; $personGroup = null;
$person_id = null; $personId = null;
$person_role = null; $personRole = null;
$credits = []; $credits = [];
foreach ($allCredits as $credit) { foreach ($allCredits as $credit) {
if ($person_group !== $credit->person_group) { if ($personGroup !== $credit->person_group) {
$person_group = $credit->person_group; $personGroup = $credit->person_group;
$person_id = $credit->person_id; $personId = $credit->person_id;
$person_role = $credit->person_role; $personRole = $credit->person_role;
$credits[$person_group] = [ $credits[$personGroup] = [
'group_label' => $credit->group_label, 'group_label' => $credit->group_label,
'persons' => [ 'persons' => [
$person_id => [ $personId => [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => 'thumbnail_url' =>
$credit->person->image->thumbnail_url, $credit->person->image->thumbnail_url,
'information_url' => 'information_url' =>
$credit->person->information_url, $credit->person->information_url,
'roles' => [ 'roles' => [
$person_role => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
@ -97,7 +97,7 @@ class PageController extends BaseController
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title} " ? "{$credit->podcast->title} "
: '') . : '') .
$credit->episode $credit->episode
->title . ->title .
@ -117,16 +117,16 @@ class PageController extends BaseController
], ],
], ],
]; ];
} elseif ($person_id !== $credit->person_id) { } elseif ($personId !== $credit->person_id) {
$person_id = $credit->person_id; $personId = $credit->person_id;
$person_role = $credit->person_role; $personRole = $credit->person_role;
$credits[$person_group]['persons'][$person_id] = [ $credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => 'thumbnail_url' =>
$credit->person->image->thumbnail_url, $credit->person->image->thumbnail_url,
'information_url' => $credit->person->information_url, 'information_url' => $credit->person->information_url,
'roles' => [ 'roles' => [
$person_role => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
@ -135,7 +135,7 @@ class PageController extends BaseController
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title} " ? "{$credit->podcast->title} "
: '') . : '') .
$credit->episode->title . $credit->episode->title .
episode_numbering( episode_numbering(
@ -151,10 +151,10 @@ class PageController extends BaseController
], ],
], ],
]; ];
} elseif ($person_role !== $credit->person_role) { } elseif ($personRole !== $credit->person_role) {
$person_role = $credit->person_role; $personRole = $credit->person_role;
$credits[$person_group]['persons'][$person_id]['roles'][ $credits[$personGroup]['persons'][$personId]['roles'][
$person_role $personRole
] = [ ] = [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
@ -164,7 +164,7 @@ class PageController extends BaseController
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title} " ? "{$credit->podcast->title} "
: '') . : '') .
$credit->episode->title . $credit->episode->title .
episode_numbering( episode_numbering(
@ -178,15 +178,15 @@ class PageController extends BaseController
], ],
]; ];
} else { } else {
$credits[$person_group]['persons'][$person_id]['roles'][ $credits[$personGroup]['persons'][$personId]['roles'][
$person_role $personRole
]['is_in'][] = [ ]['is_in'][] = [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title} " ? "{$credit->podcast->title} "
: '') . : '') .
$credit->episode->title . $credit->episode->title .
episode_numbering( episode_numbering(

View File

@ -142,7 +142,7 @@ class AddPodcasts extends Migration
'constraint' => 32, 'constraint' => 32,
'null' => true, 'null' => true,
], ],
'location_osm_id' => [ 'location_osm' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 12, 'constraint' => 12,
'null' => true, 'null' => true,

View File

@ -137,7 +137,7 @@ class AddEpisodes extends Migration
'constraint' => 32, 'constraint' => 32,
'null' => true, 'null' => true,
], ],
'location_osm_id' => [ 'location_osm' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 12, 'constraint' => 12,
'null' => true, 'null' => true,

View File

@ -35,7 +35,8 @@ class AddSoundbites extends Migration
'type' => 'DECIMAL(8,3)', 'type' => 'DECIMAL(8,3)',
], ],
'duration' => [ 'duration' => [
'type' => 'DECIMAL(8,3)', // soundbite duration cannot be higher than 9999,999 seconds ~ 2.77 hours
'type' => 'DECIMAL(7,3)',
], ],
'label' => [ 'label' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',

View File

@ -44,7 +44,7 @@ class AddEpisodesPersons extends Migration
'constraint' => 32, 'constraint' => 32,
], ],
]); ]);
$this->forge->addKey('id', true); $this->forge->addPrimaryKey('id', true);
$this->forge->addUniqueKey([ $this->forge->addUniqueKey([
'podcast_id', 'podcast_id',
'episode_id', 'episode_id',

View File

@ -52,12 +52,12 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$date < strtotime('now'); $date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day') $date = strtotime(date('Y-m-d', $date) . ' +1 day')
) { ) {
$analytics_podcasts = []; $analyticsPodcasts = [];
$analytics_podcasts_by_hour = []; $analyticsPodcastsByHour = [];
$analytics_podcasts_by_country = []; $analyticsPodcastsByCountry = [];
$analytics_podcasts_by_episode = []; $analyticsPodcastsByEpisode = [];
$analytics_podcasts_by_player = []; $analyticsPodcastsByPlayer = [];
$analytics_podcasts_by_region = []; $analyticsPodcastsByRegion = [];
$episodes = (new EpisodeModel()) $episodes = (new EpisodeModel())
->where([ ->where([
@ -72,9 +72,9 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$probability1 = (int) floor(exp(3 - $age / 40)) + 1; $probability1 = (int) floor(exp(3 - $age / 40)) + 1;
for ( for (
$num_line = 0; $lineNumber = 0;
$num_line < rand(1, $probability1); $lineNumber < rand(1, $probability1);
++$num_line ++$lineNumber
) { ) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
@ -129,7 +129,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$hits = rand(0, $probability2); $hits = rand(0, $probability2);
$analytics_podcasts[] = [ $analyticsPodcasts[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600), 'duration' => rand(60, 3600),
@ -137,26 +137,26 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'hits' => $hits, 'hits' => $hits,
'unique_listeners' => $hits, 'unique_listeners' => $hits,
]; ];
$analytics_podcasts_by_hour[] = [ $analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'hour' => rand(0, 23), 'hour' => rand(0, 23),
'hits' => $hits, 'hits' => $hits,
]; ];
$analytics_podcasts_by_country[] = [ $analyticsPodcastsByCountry[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'country_code' => $countryCode, 'country_code' => $countryCode,
'hits' => $hits, 'hits' => $hits,
]; ];
$analytics_podcasts_by_episode[] = [ $analyticsPodcastsByEpisode[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'episode_id' => $episode->id, 'episode_id' => $episode->id,
'age' => $age, 'age' => $age,
'hits' => $hits, 'hits' => $hits,
]; ];
$analytics_podcasts_by_player[] = [ $analyticsPodcastsByPlayer[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'service' => $service, 'service' => $service,
@ -166,7 +166,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'is_bot' => $isBot, 'is_bot' => $isBot,
'hits' => $hits, 'hits' => $hits,
]; ];
$analytics_podcasts_by_region[] = [ $analyticsPodcastsByRegion[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'country_code' => $countryCode, 'country_code' => $countryCode,
@ -180,27 +180,27 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$this->db $this->db
->table('analytics_podcasts') ->table('analytics_podcasts')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts); ->insertBatch($analyticsPodcasts);
$this->db $this->db
->table('analytics_podcasts_by_hour') ->table('analytics_podcasts_by_hour')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts_by_hour); ->insertBatch($analyticsPodcastsByHour);
$this->db $this->db
->table('analytics_podcasts_by_country') ->table('analytics_podcasts_by_country')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts_by_country); ->insertBatch($analyticsPodcastsByCountry);
$this->db $this->db
->table('analytics_podcasts_by_episode') ->table('analytics_podcasts_by_episode')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts_by_episode); ->insertBatch($analyticsPodcastsByEpisode);
$this->db $this->db
->table('analytics_podcasts_by_player') ->table('analytics_podcasts_by_player')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts_by_player); ->insertBatch($analyticsPodcastsByPlayer);
$this->db $this->db
->table('analytics_podcasts_by_region') ->table('analytics_podcasts_by_region')
->ignore(true) ->ignore(true)
->insertBatch($analytics_podcasts_by_region); ->insertBatch($analyticsPodcastsByRegion);
} }
} else { } else {
echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n"; echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";

View File

@ -192,9 +192,9 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
$date < strtotime('now'); $date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day') $date = strtotime(date('Y-m-d', $date) . ' +1 day')
) { ) {
$website_by_browser = []; $websiteByBrowser = [];
$website_by_entry_page = []; $websiteByEntryPage = [];
$website_by_referer = []; $websiteByReferer = [];
$episodes = (new EpisodeModel()) $episodes = (new EpisodeModel())
->where([ ->where([
@ -209,9 +209,9 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
$probability1 = (int) floor(exp(3 - $age / 40)) + 1; $probability1 = (int) floor(exp(3 - $age / 40)) + 1;
for ( for (
$num_line = 0; $lineNumber = 0;
$num_line < rand(1, $probability1); $lineNumber < rand(1, $probability1);
++$num_line ++$lineNumber
) { ) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
@ -228,19 +228,19 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
$hits = rand(0, $probability2); $hits = rand(0, $probability2);
$website_by_browser[] = [ $websiteByBrowser[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'browser' => $browser, 'browser' => $browser,
'hits' => $hits, 'hits' => $hits,
]; ];
$website_by_entry_page[] = [ $websiteByEntryPage[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'entry_page_url' => $episode->link, 'entry_page_url' => $episode->link,
'hits' => $hits, 'hits' => $hits,
]; ];
$website_by_referer[] = [ $websiteByReferer[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'referer_url' => 'referer_url' =>
@ -254,15 +254,15 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
$this->db $this->db
->table('analytics_website_by_browser') ->table('analytics_website_by_browser')
->ignore(true) ->ignore(true)
->insertBatch($website_by_browser); ->insertBatch($websiteByBrowser);
$this->db $this->db
->table('analytics_website_by_entry_page') ->table('analytics_website_by_entry_page')
->ignore(true) ->ignore(true)
->insertBatch($website_by_entry_page); ->insertBatch($websiteByEntryPage);
$this->db $this->db
->table('analytics_website_by_referer') ->table('analytics_website_by_referer')
->ignore(true) ->ignore(true)
->insertBatch($website_by_referer); ->insertBatch($websiteByReferer);
} }
} else { } else {
echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n"; echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";

View File

@ -18,7 +18,7 @@ use RuntimeException;
*/ */
class Actor extends ActivityPubActor class Actor extends ActivityPubActor
{ {
protected ?Podcast $podcast; protected ?Podcast $podcast = null;
protected bool $is_podcast; protected bool $is_podcast;
public function getIsPodcast(): bool public function getIsPodcast(): bool

View File

@ -21,7 +21,7 @@ use CodeIgniter\Entity\Entity;
*/ */
class Category extends Entity class Category extends Entity
{ {
protected ?Category $parent; protected ?Category $parent = null;
/** /**
* @var array<string, string> * @var array<string, string>

View File

@ -29,9 +29,9 @@ use CodeIgniter\Entity\Entity;
*/ */
class Credit extends Entity class Credit extends Entity
{ {
protected ?Person $person; protected ?Person $person = null;
protected ?Podcast $podcast; protected ?Podcast $podcast = null;
protected ?Episode $episode; protected ?Episode $episode = null;
protected string $group_label; protected string $group_label;
protected string $role_label; protected string $role_label;

View File

@ -58,10 +58,10 @@ use RuntimeException;
* @property int $season_number * @property int $season_number
* @property string $type * @property string $type
* @property bool $is_blocked * @property bool $is_blocked
* @property Location $location * @property Location|null $location
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm_id * @property string|null $location_osm
* @property array|null $custom_rss * @property array|null $custom_rss
* @property string $custom_rss_string * @property string $custom_rss_string
* @property int $favourites_total * @property int $favourites_total
@ -90,7 +90,7 @@ class Episode extends Entity
protected string $audio_file_opengraph_url; protected string $audio_file_opengraph_url;
protected string $embeddable_player_url; protected string $embeddable_player_url;
protected Image $image; protected Image $image;
protected ?string $description; protected ?string $description = null;
protected File $transcript_file; protected File $transcript_file;
protected File $chapters_file; protected File $chapters_file;
@ -109,9 +109,9 @@ class Episode extends Entity
*/ */
protected $notes = []; protected $notes = [];
protected ?Location $location; protected ?Location $location = null;
protected string $custom_rss_string; protected string $custom_rss_string;
protected string $publication_status; protected ?string $publication_status = null;
/** /**
* @var string[] * @var string[]
@ -152,7 +152,7 @@ class Episode extends Entity
'is_blocked' => 'boolean', 'is_blocked' => 'boolean',
'location_name' => '?string', 'location_name' => '?string',
'location_geo' => '?string', 'location_geo' => '?string',
'location_osm_id' => '?string', 'location_osm' => '?string',
'custom_rss' => '?json-array', 'custom_rss' => '?json-array',
'favourites_total' => 'integer', 'favourites_total' => 'integer',
'reblogs_total' => 'integer', 'reblogs_total' => 'integer',
@ -202,7 +202,7 @@ class Episode extends Entity
{ {
helper(['media', 'id3']); helper(['media', 'id3']);
$audio_metadata = get_file_tags($audioFile); $audioMetadata = get_file_tags($audioFile);
$this->attributes['audio_file_path'] = save_media( $this->attributes['audio_file_path'] = save_media(
$audioFile, $audioFile,
@ -210,11 +210,11 @@ class Episode extends Entity
$this->attributes['slug'], $this->attributes['slug'],
); );
$this->attributes['audio_file_duration'] = $this->attributes['audio_file_duration'] =
$audio_metadata['playtime_seconds']; $audioMetadata['playtime_seconds'];
$this->attributes['audio_file_mimetype'] = $audio_metadata['mime_type']; $this->attributes['audio_file_mimetype'] = $audioMetadata['mime_type'];
$this->attributes['audio_file_size'] = $audio_metadata['filesize']; $this->attributes['audio_file_size'] = $audioMetadata['filesize'];
$this->attributes['audio_file_header_size'] = $this->attributes['audio_file_header_size'] =
$audio_metadata['avdataoffset']; $audioMetadata['avdataoffset'];
return $this; return $this;
} }
@ -471,10 +471,8 @@ class Episode extends Entity
$this->getPodcast()->partner_link_url !== null && $this->getPodcast()->partner_link_url !== null &&
$this->getPodcast()->partner_image_url !== null $this->getPodcast()->partner_image_url !== null
) { ) {
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink( $descriptionHtml .= "<div><a href=\"{$this->getPartnerLink($serviceSlug,
$serviceSlug, )}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl($serviceSlug,
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
$serviceSlug,
)}\" alt=\"Partner image\" /></a></div>"; )}\" alt=\"Partner image\" /></a></div>";
} }
@ -504,47 +502,41 @@ class Episode extends Entity
public function getPublicationStatus(): string public function getPublicationStatus(): string
{ {
if ($this->publication_status !== '') { if ($this->publication_status === null) {
return $this->publication_status; if ($this->published_at === null) {
$this->publication_status = 'not_published';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
$this->publication_status = 'scheduled';
}
} }
if ($this->published_at === null) { return $this->publication_status;
return 'not_published';
}
helper('date');
if ($this->published_at->isBefore(Time::now())) {
return 'published';
}
return 'scheduled';
} }
/** /**
* Saves the location name and fetches OpenStreetMap info * Saves the location name and fetches OpenStreetMap info
*/ */
public function setLocation(?string $newLocationName = null): static public function setLocation(?Location $location = null): static
{ {
if ($newLocationName === null) { if ($location === null) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm_id'] = null; $this->attributes['location_osm'] = null;
return $this;
} }
helper('location');
$oldLocationName = $this->attributes['location_name'];
if ( if (
$oldLocationName === null || !isset($this->attributes['location_name']) ||
$oldLocationName !== $newLocationName $this->attributes['location_name'] !== $location->name
) { ) {
$this->attributes['location_name'] = $newLocationName; $location->fetchOsmLocation();
if ($location = fetch_osm_location($newLocationName)) { $this->attributes['location_name'] = $location->name;
$this->attributes['location_geo'] = $location['geo']; $this->attributes['location_geo'] = $location->geo;
$this->attributes['location_osm_id'] = $location['osm_id']; $this->attributes['location_osm'] = $location->osm;
}
} }
return $this; return $this;
@ -557,11 +549,11 @@ class Episode extends Entity
} }
if ($this->location === null) { if ($this->location === null) {
$this->location = new Location([ $this->location = new Location(
'name' => $this->location_name, $this->location_name,
'geo' => $this->location_geo, $this->location_geo,
'osm_id' => $this->location_osm_id, $this->location_osm,
]); );
} }
return $this->location; return $this->location;
@ -645,9 +637,9 @@ class Episode extends Entity
} }
return rtrim($this->getPodcast()->partner_image_url, '/') . return rtrim($this->getPodcast()->partner_image_url, '/') .
'?pid=' . '?pid=' .
$this->getPodcast()->partner_id . $this->getPodcast()->partner_id .
'&guid=' . '&guid=' .
urlencode($this->attributes['guid']); urlencode($this->attributes['guid']);
} }
} }

View File

@ -36,7 +36,7 @@ use RuntimeException;
class Image extends Entity class Image extends Entity
{ {
protected Images $config; protected Images $config;
protected ?File $file; protected ?File $file = null;
protected string $dirname; protected string $dirname;
protected string $filename; protected string $filename;
protected string $extension; protected string $extension;

View File

@ -9,12 +9,13 @@
namespace App\Entities; namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use Config\Services;
/** /**
* @property string $url * @property string $url
* @property string $name * @property string $name
* @property string|null $geo * @property string|null $geo
* @property string|null $osm_id * @property string|null $osm
*/ */
class Location extends Entity class Location extends Entity
{ {
@ -23,15 +24,30 @@ class Location extends Entity
*/ */
const OSM_URL = 'https://www.openstreetmap.org/'; const OSM_URL = 'https://www.openstreetmap.org/';
/**
* @var string
*/
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
public function __construct(
protected string $name,
protected ?string $geo = null,
protected ?string $osm = null
) {
parent::__construct([
'name' => $name,
'geo' => $geo,
'osm' => $osm
]);
}
public function getUrl(): string public function getUrl(): string
{ {
if ($this->osm_id !== null) { if ($this->osm !== null) {
return self::OSM_URL . return self::OSM_URL .
['N' => 'node', 'W' => 'way', 'R' => 'relation'][ ['N' => 'node', 'W' => 'way', 'R' => 'relation'][substr($this->osm, 0, 1)] .
substr($this->osm_id, 0, 1)
] .
'/' . '/' .
substr($this->osm_id, 1); substr($this->osm, 1);
} }
if ($this->geo !== null) { if ($this->geo !== null) {
@ -42,4 +58,49 @@ class Location extends Entity
return self::OSM_URL . 'search?query=' . urlencode($this->name); return self::OSM_URL . 'search?query=' . urlencode($this->name);
} }
/**
* Fetches places from Nominatim OpenStreetMap
*
* @return array<string, string>|null
*/
public function fetchOsmLocation(): self
{
$client = Services::curlrequest();
$response = $client->request(
'GET',
self::NOMINATIM_URL .
'search.php?q=' .
urlencode($this->name) .
'&polygon_geojson=1&format=jsonv2',
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
],
],
);
$places = json_decode(
$response->getBody(),
false,
512,
JSON_THROW_ON_ERROR,
);
if ($places === []) {
return $this;
}
if (isset($places[0]->lat, $places[0]->lon)) {
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
}
if (isset($places[0]->osm_type, $places[0]->osm_id)) {
$this->attributes['osm'] = strtoupper(substr($places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
}
return $this;
}
} }

View File

@ -21,7 +21,7 @@ use RuntimeException;
*/ */
class Note extends ActivityPubNote class Note extends ActivityPubNote
{ {
protected ?Episode $episode; protected ?Episode $episode = null;
/** /**
* @var array<string, string> * @var array<string, string>

View File

@ -8,7 +8,9 @@
namespace App\Entities; namespace App\Entities;
use App\Models\PersonModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use RuntimeException;
/** /**
* @property int $id * @property int $id
@ -20,16 +22,16 @@ use CodeIgniter\Entity\Entity;
* @property string $image_mimetype * @property string $image_mimetype
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string|null $group * @property string[]|null $roles
* @property string|null $role
* @property Podcast|null $podcast
* @property Episode|null $episode
*/ */
class Person extends Entity class Person extends Entity
{ {
protected Image $image; protected Image $image;
protected ?Podcast $podcast;
protected ?Episode $episode; /**
* @var string[]|null
*/
protected ?array $roles = null;
/** /**
* @var array<string, string> * @var array<string, string>
@ -43,8 +45,6 @@ class Person extends Entity
'image_mimetype' => 'string', 'image_mimetype' => 'string',
'podcast_id' => '?integer', 'podcast_id' => '?integer',
'episode_id' => '?integer', 'episode_id' => '?integer',
'group' => '?string',
'role' => '?string',
'created_by' => 'integer', 'created_by' => 'integer',
'updated_by' => 'integer', 'updated_by' => 'integer',
]; ];
@ -73,4 +73,21 @@ class Person extends Entity
$this->attributes['image_mimetype'], $this->attributes['image_mimetype'],
); );
} }
/**
* @return stdClass[]
*/
public function getRoles(): array {
if ($this->podcast_id === null) {
throw new RuntimeException(
'Person must have a podcast_id before getting roles.',
);
}
if ($this->roles === null) {
$this->roles = (new PersonModel())->getPersonRoles($this->id, $this->podcast_id, $this->episode_id);
}
return $this->roles;
}
} }

View File

@ -27,7 +27,7 @@ use RuntimeException;
* @property string $link * @property string $link
* @property string $feed_url * @property string $feed_url
* @property string $title * @property string $title
* @property string $description Holds text only description, striped of any markdown or html special characters * @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown * @property string $description_markdown
* @property string $description_html * @property string $description_html
* @property Image $image * @property Image $image
@ -51,10 +51,10 @@ use RuntimeException;
* @property bool $is_locked * @property bool $is_locked
* @property string|null $imported_feed_url * @property string|null $imported_feed_url
* @property string|null $new_feed_url * @property string|null $new_feed_url
* @property Location $location * @property Location|null $location
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm_id * @property string|null $location_osm
* @property string|null $payment_pointer * @property string|null $payment_pointer
* @property array|null $custom_rss * @property array|null $custom_rss
* @property string $custom_rss_string * @property string $custom_rss_string
@ -78,10 +78,10 @@ use RuntimeException;
class Podcast extends Entity class Podcast extends Entity
{ {
protected string $link; protected string $link;
protected ?Actor $actor; protected ?Actor $actor = null;
protected Image $image; protected Image $image;
protected string $description; protected ?string $description = null;
protected ?Category $category; protected ?Category $category = null;
/** /**
* @var Category[] * @var Category[]
@ -123,7 +123,7 @@ class Podcast extends Entity
*/ */
protected $funding_platforms = []; protected $funding_platforms = [];
protected ?Location $location; protected ?Location $location = null;
protected string $custom_rss_string; protected string $custom_rss_string;
/** /**
@ -155,7 +155,7 @@ class Podcast extends Entity
'new_feed_url' => '?string', 'new_feed_url' => '?string',
'location_name' => '?string', 'location_name' => '?string',
'location_geo' => '?string', 'location_geo' => '?string',
'location_osm_id' => '?string', 'location_osm' => '?string',
'payment_pointer' => '?string', 'payment_pointer' => '?string',
'custom_rss' => '?json-array', 'custom_rss' => '?json-array',
'partner_id' => '?string', 'partner_id' => '?string',
@ -331,17 +331,17 @@ class Podcast extends Entity
public function getDescription(): string public function getDescription(): string
{ {
if ($this->description !== '') { if ($this->description === null) {
return $this->description; $this->description = trim(
(string) preg_replace(
'~\s+~',
' ',
strip_tags($this->attributes['description_html']),
),
);
} }
return trim( return $this->description;
preg_replace(
'~\s+~',
' ',
strip_tags($this->attributes['description_html']),
),
);
} }
/** /**
@ -451,28 +451,25 @@ class Podcast extends Entity
/** /**
* Saves the location name and fetches OpenStreetMap info * Saves the location name and fetches OpenStreetMap info
*/ */
public function setLocation(?string $newLocationName = null): static public function setLocation(?Location $location = null): static
{ {
if ($newLocationName === null) { if ($location === null) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm_id'] = null; $this->attributes['location_osm'] = null;
return $this;
} }
helper('location');
$oldLocationName = $this->attributes['location_name'];
if ( if (
$oldLocationName === null || !isset($this->attributes['location_name']) ||
$oldLocationName !== $newLocationName $this->attributes['location_name'] !== $location->name
) { ) {
$this->attributes['location_name'] = $newLocationName; $location->fetchOsmLocation();
if ($location = fetch_osm_location($newLocationName)) { $this->attributes['location_name'] = $location->name;
$this->attributes['location_geo'] = $location['geo']; $this->attributes['location_geo'] = $location->geo;
$this->attributes['location_osm_id'] = $location['osm_id']; $this->attributes['location_osm'] = $location->osm;
}
} }
return $this; return $this;
@ -485,11 +482,11 @@ class Podcast extends Entity
} }
if ($this->location === null) { if ($this->location === null) {
$this->location = new Location([ $this->location = new Location(
'name' => $this->location_name, $this->location_name,
'geo' => $this->location_geo, $this->location_geo,
'osm_id' => $this->location_osm_id, $this->location_osm,
]); );
} }
return $this->location; return $this->location;

View File

@ -27,9 +27,9 @@ use Myth\Auth\Entities\User as MythAuthUser;
class User extends MythAuthUser class User extends MythAuthUser
{ {
/** /**
* @var Podcast[] * @var Podcast[]|null
*/ */
protected $podcasts = []; protected ?array $podcasts = null;
/** /**
* Array of field names and the type of value to cast them as * Array of field names and the type of value to cast them as

View File

@ -7,6 +7,7 @@
*/ */
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Person;
use CodeIgniter\View\Table; use CodeIgniter\View\Table;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
@ -291,13 +292,11 @@ if (!function_exists('publication_button')) {
* Publication button component * Publication button component
* *
* Displays the appropriate publication button depending on the publication status. * Displays the appropriate publication button depending on the publication status.
*
* @param boolean $publicationStatus the episode's publication status *
*/ */
function publication_button( function publication_button(
int $podcastId, int $podcastId,
int $episodeId, int $episodeId,
bool $publicationStatus string $publicationStatus
): string { ): string {
switch ($publicationStatus) { switch ($publicationStatus) {
case 'not_published': case 'not_published':
@ -416,3 +415,59 @@ if (!function_exists('location_link')) {
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (!function_exists('person_list')) {
/**
* Returns list of persons images
*
* @param Person[] $persons
*/
function person_list(array $persons, string $class = ''): string
{
if ($persons === []) {
return '';
}
$personList = "<div class='flex w-full space-x-2 overflow-y-auto {$class}'>";
foreach ($persons as $person) {
$personList .= anchor(
$person->information_url ?? '#',
"<img
src='{$person->image->thumbnail_url}'
alt='$person->full_name'
class='object-cover w-12 h-12 rounded-full' />",
[
'class' =>
'flex-shrink-0 focus:outline-none focus:ring focus:ring-inset',
'target' => '_blank',
'rel' => 'noreferrer noopener',
'title' =>
'<strong>' .
$person->full_name .
'</strong>' .
implode(
array_map(function ($role) {
return '<br />' .
lang(
'PersonsTaxonomy.persons.' .
$role->group .
'.roles.' .
$role->role .
'.label',
);
}, $person->roles),
),
'data-toggle' => 'tooltip',
'data-placement' => 'bottom',
],
);
}
$personList .= '</div>';
return $personList;
}
}
// ------------------------------------------------------------------------

View File

@ -99,14 +99,14 @@ if (!function_exists('form_label')) {
/** /**
* Form Label Tag * Form Label Tag
* *
* @param string $label_text The text to appear onscreen * @param string $text The text to appear onscreen
* @param string $id The id the label applies to * @param string $id The id the label applies to
* @param array<string, string> $attributes Additional attributes * @param array<string, string> $attributes Additional attributes
* @param string $hintText Hint text to add next to the label * @param string $hintText Hint text to add next to the label
* @param boolean $isOptional adds an optional text if true * @param boolean $isOptional adds an optional text if true
*/ */
function form_label( function form_label(
string $label_text = '', string $text = '',
string $id = '', string $id = '',
array $attributes = [], array $attributes = [],
string $hintText = '', string $hintText = '',
@ -124,19 +124,19 @@ if (!function_exists('form_label')) {
} }
} }
$label_content = $label_text; $labelContent = $text;
if ($isOptional) { if ($isOptional) {
$label_content .= $labelContent .=
'<small class="ml-1 lowercase">(' . '<small class="ml-1 lowercase">(' .
lang('Common.optional') . lang('Common.optional') .
')</small>'; ')</small>';
} }
if ($hintText !== '') { if ($hintText !== '') {
$label_content .= hint_tooltip($hintText, 'ml-1'); $labelContent .= hint_tooltip($hintText, 'ml-1');
} }
return $label . '>' . $label_content . '</label>'; return $label . '>' . $labelContent . '</label>';
} }
} }

View File

@ -54,9 +54,9 @@ if (!function_exists('write_audio_file_tags')) {
$APICdata = file_get_contents($cover->getRealPath()); $APICdata = file_get_contents($cover->getRealPath());
// TODO: variables used for podcast specific tags // TODO: variables used for podcast specific tags
// $podcast_url = $episode->podcast->link; // $podcastUrl = $episode->podcast->link;
// $podcast_feed_url = $episode->podcast->feed_url; // $podcastFeedUrl = $episode->podcast->feed_url;
// $episode_media_url = $episode->link; // $episodeMediaUrl = $episode->link;
// populate data array // populate data array
$TagData = [ $TagData = [
@ -74,7 +74,7 @@ if (!function_exists('write_audio_file_tags')) {
], ],
'genre' => ['Podcast'], 'genre' => ['Podcast'],
'comment' => [$episode->description], 'comment' => [$episode->description],
'track_number' => [strval($episode->number)], 'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright], 'copyright_message' => [$episode->podcast->copyright],
'publisher' => [ 'publisher' => [
empty($episode->podcast->publisher) empty($episode->podcast->publisher)

View File

@ -1,62 +0,0 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use Config\Services;
if (!function_exists('fetch_osm_location')) {
/**
* Fetches places from Nominatim OpenStreetMap
*
* TODO: move this to Location object?
*
* @return array<string, string>|null
*/
function fetch_osm_location(string $locationName): ?array
{
$osmObject = null;
try {
$client = Services::curlrequest();
$response = $client->request(
'GET',
'https://nominatim.openstreetmap.org/search.php?q=' .
urlencode($locationName) .
'&polygon_geojson=1&format=jsonv2',
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
],
],
);
$places = json_decode(
$response->getBody(),
true,
512,
JSON_THROW_ON_ERROR,
);
$osmObject = [
'geo' =>
empty($places[0]['lat']) || empty($places[0]['lon'])
? null
: "geo:{$places[0]['lat']},{$places[0]['lon']}",
'osm_id' => empty($places[0]['osm_type'])
? null
: strtoupper(substr($places[0]['osm_type'], 0, 1)) .
$places[0]['osm_id'],
];
} catch (Exception $exception) {
//If things go wrong the show must go on
log_message('critical', $exception);
}
return $osmObject;
}
}

View File

@ -29,7 +29,7 @@ if (!function_exists('slugify')) {
// replace non letter or digits by - // replace non letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text); $text = preg_replace('~[^\pL\d]+~u', '-', $text);
$unwanted_array = [ $unwanted = [
'Š' => 'S', 'Š' => 'S',
'š' => 's', 'š' => 's',
'Đ' => 'Dj', 'Đ' => 'Dj',
@ -107,7 +107,7 @@ if (!function_exists('slugify')) {
'/' => '-', '/' => '-',
' ' => '-', ' ' => '-',
]; ];
$text = strtr($text, $unwanted_array); $text = strtr($text, $unwanted);
// transliterate // transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

View File

@ -23,31 +23,31 @@ if (!function_exists('get_rss_feed')) {
{ {
$episodes = $podcast->episodes; $episodes = $podcast->episodes;
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$podcast_namespace = $podcastNamespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'; 'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$rss = new SimpleRSSElement( $rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunes_namespace}' xmlns:podcast='{$podcast_namespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>", "<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunesNamespace}' xmlns:podcast='{$podcastNamespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
); );
$channel = $rss->addChild('channel'); $channel = $rss->addChild('channel');
$atom_link = $channel->addChild( $atomLink = $channel->addChild(
'atom:link', 'atom:link',
null, null,
'http://www.w3.org/2005/Atom', 'http://www.w3.org/2005/Atom',
); );
$atom_link->addAttribute('href', $podcast->feed_url); $atomLink->addAttribute('href', $podcast->feed_url);
$atom_link->addAttribute('rel', 'self'); $atomLink->addAttribute('rel', 'self');
$atom_link->addAttribute('type', 'application/rss+xml'); $atomLink->addAttribute('type', 'application/rss+xml');
if ($podcast->new_feed_url !== null) { if ($podcast->new_feed_url !== null) {
$channel->addChild( $channel->addChild(
'new-feed-url', 'new-feed-url',
$podcast->new_feed_url, $podcast->new_feed_url,
$itunes_namespace, $itunesNamespace,
); );
} }
@ -65,33 +65,30 @@ if (!function_exists('get_rss_feed')) {
$channel->addChild('title', $podcast->title); $channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html); $channel->addChildWithCDATA('description', $podcast->description_html);
$itunes_image = $channel->addChild('image', null, $itunes_namespace); $itunesImage = $channel->addChild('image', null, $itunesNamespace);
// FIXME: This should be downsized to 1400x1400 // FIXME: This should be downsized to 1400x1400
$itunes_image->addAttribute('href', $podcast->image->url); $itunesImage->addAttribute('href', $podcast->image->url);
$channel->addChild('language', $podcast->language_code); $channel->addChild('language', $podcast->language_code);
if ($podcast->location !== null) { if ($podcast->location !== null) {
$locationElement = $channel->addChild( $locationElement = $channel->addChild(
'location', 'location',
htmlspecialchars($podcast->location->name), htmlspecialchars($podcast->location->name),
$podcast_namespace, $podcastNamespace,
); );
if ($podcast->location->geo !== null) { if ($podcast->location->geo !== null) {
$locationElement->addAttribute('geo', $podcast->location->geo); $locationElement->addAttribute('geo', $podcast->location->geo);
} }
if ($podcast->location->osm_id !== null) { if ($podcast->location->osm !== null) {
$locationElement->addAttribute( $locationElement->addAttribute('osm', $podcast->location->osm);
'osm',
$podcast->location->osm_id,
);
} }
} }
if ($podcast->payment_pointer !== null) { if ($podcast->payment_pointer !== null) {
$valueElement = $channel->addChild( $valueElement = $channel->addChild(
'value', 'value',
null, null,
$podcast_namespace, $podcastNamespace,
); );
$valueElement->addAttribute('type', 'webmonetization'); $valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', ''); $valueElement->addAttribute('method', '');
@ -99,7 +96,7 @@ if (!function_exists('get_rss_feed')) {
$recipientElement = $valueElement->addChild( $recipientElement = $valueElement->addChild(
'valueRecipient', 'valueRecipient',
null, null,
$podcast_namespace, $podcastNamespace,
); );
$recipientElement->addAttribute('name', $podcast->owner_name); $recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'ILP'); $recipientElement->addAttribute('type', 'ILP');
@ -113,14 +110,14 @@ if (!function_exists('get_rss_feed')) {
->addChild( ->addChild(
'locked', 'locked',
$podcast->is_locked ? 'yes' : 'no', $podcast->is_locked ? 'yes' : 'no',
$podcast_namespace, $podcastNamespace,
) )
->addAttribute('owner', $podcast->owner_email); ->addAttribute('owner', $podcast->owner_email);
if ($podcast->imported_feed_url !== null) { if ($podcast->imported_feed_url !== null) {
$channel->addChild( $channel->addChild(
'previousUrl', 'previousUrl',
$podcast->imported_feed_url, $podcast->imported_feed_url,
$podcast_namespace, $podcastNamespace,
); );
} }
@ -128,7 +125,7 @@ if (!function_exists('get_rss_feed')) {
$podcastingPlatformElement = $channel->addChild( $podcastingPlatformElement = $channel->addChild(
'id', 'id',
null, null,
$podcast_namespace, $podcastNamespace,
); );
$podcastingPlatformElement->addAttribute( $podcastingPlatformElement->addAttribute(
'platform', 'platform',
@ -152,7 +149,7 @@ if (!function_exists('get_rss_feed')) {
$socialPlatformElement = $channel->addChild( $socialPlatformElement = $channel->addChild(
'social', 'social',
$socialPlatform->link_content, $socialPlatform->link_content,
$podcast_namespace, $podcastNamespace,
); );
$socialPlatformElement->addAttribute( $socialPlatformElement->addAttribute(
'platform', 'platform',
@ -170,7 +167,7 @@ if (!function_exists('get_rss_feed')) {
$fundingPlatformElement = $channel->addChild( $fundingPlatformElement = $channel->addChild(
'funding', 'funding',
$fundingPlatform->link_content, $fundingPlatform->link_content,
$podcast_namespace, $podcastNamespace,
); );
$fundingPlatformElement->addAttribute( $fundingPlatformElement->addAttribute(
'platform', 'platform',
@ -188,7 +185,7 @@ if (!function_exists('get_rss_feed')) {
$podcastPersonElement = $channel->addChild( $podcastPersonElement = $channel->addChild(
'person', 'person',
htmlspecialchars($podcastPerson->full_name), htmlspecialchars($podcastPerson->full_name),
$podcast_namespace, $podcastNamespace,
); );
if ( if (
@ -242,29 +239,29 @@ if (!function_exists('get_rss_feed')) {
$channel->addChild( $channel->addChild(
'explicit', 'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false', $podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace, $itunesNamespace,
); );
$channel->addChild( $channel->addChild(
'author', 'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name, $podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunes_namespace, $itunesNamespace,
); );
$channel->addChild('link', $podcast->link); $channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunes_namespace); $owner = $channel->addChild('owner', null, $itunesNamespace);
$owner->addChild('name', $podcast->owner_name, $itunes_namespace); $owner->addChild('name', $podcast->owner_name, $itunesNamespace);
$owner->addChild('email', $podcast->owner_email, $itunes_namespace); $owner->addChild('email', $podcast->owner_email, $itunesNamespace);
$channel->addChild('type', $podcast->type, $itunes_namespace); $channel->addChild('type', $podcast->type, $itunesNamespace);
$podcast->copyright && $podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright); $channel->addChild('copyright', $podcast->copyright);
$podcast->is_blocked && $podcast->is_blocked &&
$channel->addChild('block', 'Yes', $itunes_namespace); $channel->addChild('block', 'Yes', $itunesNamespace);
$podcast->is_completed && $podcast->is_completed &&
$channel->addChild('complete', 'Yes', $itunes_namespace); $channel->addChild('complete', 'Yes', $itunesNamespace);
$image = $channel->addChild('image'); $image = $channel->addChild('image');
$image->addChild('url', $podcast->image->feed_url); $image->addChild('url', $podcast->image->feed_url);
@ -304,7 +301,7 @@ if (!function_exists('get_rss_feed')) {
$locationElement = $item->addChild( $locationElement = $item->addChild(
'location', 'location',
htmlspecialchars($episode->location->name), htmlspecialchars($episode->location->name),
$podcast_namespace, $podcastNamespace,
); );
if ($episode->location->geo !== null) { if ($episode->location->geo !== null) {
$locationElement->addAttribute( $locationElement->addAttribute(
@ -312,10 +309,10 @@ if (!function_exists('get_rss_feed')) {
$episode->location->geo, $episode->location->geo,
); );
} }
if ($episode->location->osm_id !== null) { if ($episode->location->osm !== null) {
$locationElement->addAttribute( $locationElement->addAttribute(
'osm', 'osm',
$episode->location->osm_id, $episode->location->osm,
); );
} }
} }
@ -326,15 +323,15 @@ if (!function_exists('get_rss_feed')) {
$item->addChild( $item->addChild(
'duration', 'duration',
$episode->audio_file_duration, $episode->audio_file_duration,
$itunes_namespace, $itunesNamespace,
); );
$item->addChild('link', $episode->link); $item->addChild('link', $episode->link);
$episode_itunes_image = $item->addChild( $episodeItunesImage = $item->addChild(
'image', 'image',
null, null,
$itunes_namespace, $itunesNamespace,
); );
$episode_itunes_image->addAttribute( $episodeItunesImage->addAttribute(
'href', 'href',
$episode->image->feed_url, $episode->image->feed_url,
); );
@ -345,24 +342,24 @@ if (!function_exists('get_rss_feed')) {
$episode->parental_advisory === 'explicit' $episode->parental_advisory === 'explicit'
? 'true' ? 'true'
: 'false', : 'false',
$itunes_namespace, $itunesNamespace,
); );
$episode->number && $episode->number &&
$item->addChild('episode', $episode->number, $itunes_namespace); $item->addChild('episode', $episode->number, $itunesNamespace);
$episode->season_number && $episode->season_number &&
$item->addChild( $item->addChild(
'season', 'season',
$episode->season_number, $episode->season_number,
$itunes_namespace, $itunesNamespace,
); );
$item->addChild('episodeType', $episode->type, $itunes_namespace); $item->addChild('episodeType', $episode->type, $itunesNamespace);
if ($episode->transcript_file_url) { if ($episode->transcript_file_url) {
$transcriptElement = $item->addChild( $transcriptElement = $item->addChild(
'transcript', 'transcript',
null, null,
$podcast_namespace, $podcastNamespace,
); );
$transcriptElement->addAttribute( $transcriptElement->addAttribute(
'url', 'url',
@ -387,7 +384,7 @@ if (!function_exists('get_rss_feed')) {
$chaptersElement = $item->addChild( $chaptersElement = $item->addChild(
'chapters', 'chapters',
null, null,
$podcast_namespace, $podcastNamespace,
); );
$chaptersElement->addAttribute( $chaptersElement->addAttribute(
'url', 'url',
@ -403,7 +400,7 @@ if (!function_exists('get_rss_feed')) {
$soundbiteElement = $item->addChild( $soundbiteElement = $item->addChild(
'soundbite', 'soundbite',
empty($soundbite->label) ? null : $soundbite->label, empty($soundbite->label) ? null : $soundbite->label,
$podcast_namespace, $podcastNamespace,
); );
$soundbiteElement->addAttribute( $soundbiteElement->addAttribute(
'start_time', 'start_time',
@ -419,7 +416,7 @@ if (!function_exists('get_rss_feed')) {
$episodePersonElement = $item->addChild( $episodePersonElement = $item->addChild(
'person', 'person',
htmlspecialchars($episodePerson->full_name), htmlspecialchars($episodePerson->full_name),
$podcast_namespace, $podcastNamespace,
); );
if ( if (
!empty($episodePerson->role) && !empty($episodePerson->role) &&
@ -461,7 +458,7 @@ if (!function_exists('get_rss_feed')) {
} }
$episode->is_blocked && $episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace); $item->addChild('block', 'Yes', $itunesNamespace);
if (!empty($episode->custom_rss)) { if (!empty($episode->custom_rss)) {
array_to_rss( array_to_rss(
@ -483,10 +480,10 @@ if (!function_exists('add_category_tag')) {
*/ */
function add_category_tag(SimpleXMLElement $node, Category $category): void function add_category_tag(SimpleXMLElement $node, Category $category): void
{ {
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunes_category = $node->addChild('category', '', $itunes_namespace); $itunesCategory = $node->addChild('category', '', $itunesNamespace);
$itunes_category->addAttribute( $itunesCategory->addAttribute(
'text', 'text',
$category->parent !== null $category->parent !== null
? $category->parent->apple_category ? $category->parent->apple_category
@ -494,12 +491,12 @@ if (!function_exists('add_category_tag')) {
); );
if ($category->parent !== null) { if ($category->parent !== null) {
$itunes_category_child = $itunes_category->addChild( $itunesCategoryChild = $itunesCategory->addChild(
'category', 'category',
'', '',
$itunes_namespace, $itunesNamespace,
); );
$itunes_category_child->addAttribute( $itunesCategoryChild->addAttribute(
'text', 'text',
$category->apple_category, $category->apple_category,
); );

View File

@ -16,16 +16,16 @@ if (!function_exists('icon')) {
*/ */
function icon(string $name, string $class = ''): string function icon(string $name, string $class = ''): string
{ {
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg'); $svgContents = file_get_contents('assets/icons/' . $name . '.svg');
if ($class !== '') { if ($class !== '') {
$svg_contents = str_replace( $svgContents = str_replace(
'<svg', '<svg',
'<svg class="' . $class . '"', '<svg class="' . $class . '"',
$svg_contents, $svgContents,
); );
} }
return $svg_contents; return $svgContents;
} }
} }
@ -39,14 +39,14 @@ if (!function_exists('svg')) {
*/ */
function svg(string $name, ?string $class = null): string function svg(string $name, ?string $class = null): string
{ {
$svg_contents = file_get_contents('assets/images/' . $name . '.svg'); $svgContents = file_get_contents('assets/images/' . $name . '.svg');
if ($class) { if ($class) {
$svg_contents = str_replace( $svgContents = str_replace(
'<svg', '<svg',
'<svg class="' . $class . '"', '<svg class="' . $class . '"',
$svg_contents, $svgContents,
); );
} }
return $svg_contents; return $svgContents;
} }
} }

View File

@ -27,22 +27,6 @@ if (!function_exists('host_url')) {
} }
} }
if (!function_exists('current_season_url')) {
/**
* Return the podcast URL with season number to use in views
*/
function current_season_url(): string
{
$season_query_string = '';
if (isset($_GET['season'])) {
$season_query_string = '?season=' . $_GET['season'];
} elseif (isset($_GET['year'])) {
$season_query_string = '?year=' . $_GET['year'];
}
return current_url() . $season_query_string;
}
}
//-------------------------------------------------------------------- //--------------------------------------------------------------------
if (!function_exists('extract_params_from_episode_uri')) { if (!function_exists('extract_params_from_episode_uri')) {

View File

@ -85,7 +85,7 @@ class ActivityRequest
$date = Time::now('GMT')->format('D, d M Y H:i:s T'); $date = Time::now('GMT')->format('D, d M Y H:i:s T');
$digest = 'SHA-256=' . base64_encode($this->getBodyDigest()); $digest = 'SHA-256=' . base64_encode($this->getBodyDigest());
$contentType = $this->options['headers']['Content-Type']; $contentType = $this->options['headers']['Content-Type'];
$contentLength = strval(strlen($this->request->getBody())); $contentLength = (string) strlen($this->request->getBody());
$userAgent = 'Castopod'; $userAgent = 'Castopod';
$plainText = "(request-target): post {$path}\nhost: {$host}\ndate: {$date}\ndigest: {$digest}\ncontent-type: {$contentType}\ncontent-length: {$contentLength}\nuser-agent: {$userAgent}"; $plainText = "(request-target): post {$path}\nhost: {$host}\ndate: {$date}\ndigest: {$digest}\ncontent-type: {$contentType}\ncontent-length: {$contentLength}\nuser-agent: {$userAgent}";

View File

@ -182,7 +182,7 @@ if (!function_exists('create_preview_card_from_url')) {
// Check that, at least, the url and title are set // Check that, at least, the url and title are set
if ($media->url && $media->title) { if ($media->url && $media->title) {
$preview_card = new PreviewCard([ $newPreviewCard = new PreviewCard([
'url' => (string) $url, 'url' => (string) $url,
'title' => $media->title, 'title' => $media->title,
'description' => $media->description, 'description' => $media->description,
@ -199,15 +199,15 @@ if (!function_exists('create_preview_card_from_url')) {
if ( if (
!($newPreviewCardId = model('PreviewCardModel')->insert( !($newPreviewCardId = model('PreviewCardModel')->insert(
$preview_card, $newPreviewCard,
true, true,
)) ))
) { ) {
return null; return null;
} }
$preview_card->id = $newPreviewCardId; $newPreviewCard->id = $newPreviewCardId;
return $preview_card; return $newPreviewCard;
} }
} }

View File

@ -45,7 +45,7 @@ class HttpSignature
public function __construct(IncomingRequest $request = null) public function __construct(IncomingRequest $request = null)
{ {
if (is_null($request)) { if ($request === null) {
$request = Services::request(); $request = Services::request();
} }

View File

@ -39,18 +39,21 @@ class AnalyticsController extends Controller
); );
} }
public function getData(int $podcastId, int $episodeId): ResponseInterface public function getData(
{ int $podcastId,
$analytics_model = new $this->className(); ?int $episodeId = null
): ResponseInterface {
$analyticsModel = new $this->className();
$methodName = $this->methodName; $methodName = $this->methodName;
if ($episodeId !== 0) {
if ($episodeId === null) {
return $this->response->setJSON( return $this->response->setJSON(
$analytics_model->$methodName($podcastId, $episodeId), $analyticsModel->$methodName($podcastId),
); );
} }
return $this->response->setJSON( return $this->response->setJSON(
$analytics_model->$methodName($podcastId), $analyticsModel->$methodName($podcastId, $episodeId),
); );
} }
} }

View File

@ -26,15 +26,15 @@ class SimpleRSSElement extends SimpleXMLElement
string $value = '', string $value = '',
?string $namespace = null ?string $namespace = null
) { ) {
$new_child = parent::addChild($name, '', $namespace); $newChild = parent::addChild($name, '', $namespace);
if ($new_child !== null) { if ($newChild !== null) {
$node = dom_import_simplexml($new_child); $node = dom_import_simplexml($newChild);
$no = $node->ownerDocument; $no = $node->ownerDocument;
$node->appendChild($no->createCDATASection($value)); $node->appendChild($no->createCDATASection($value));
} }
return $new_child; return $newChild;
} }
/** /**
@ -49,14 +49,14 @@ class SimpleRSSElement extends SimpleXMLElement
*/ */
public function addChild($name, $value = null, $namespace = null) public function addChild($name, $value = null, $namespace = null)
{ {
$new_child = parent::addChild($name, '', $namespace); $newChild = parent::addChild($name, '', $namespace);
if ($new_child !== null) { if ($newChild !== null) {
$node = dom_import_simplexml($new_child); $node = dom_import_simplexml($newChild);
$no = $node->ownerDocument; $no = $node->ownerDocument;
$node->appendChild($no->createTextNode(esc($value))); $node->appendChild($no->createTextNode(esc($value)));
} }
return $new_child; return $newChild;
} }
} }

View File

@ -83,7 +83,7 @@ class EpisodeModel extends Model
'is_blocked', 'is_blocked',
'location_name', 'location_name',
'location_geo', 'location_geo',
'location_osm_id', 'location_osm',
'custom_rss', 'custom_rss',
'favourites_total', 'favourites_total',
'reblogs_total', 'reblogs_total',

View File

@ -19,6 +19,7 @@ class PersonModel extends Model
* @var string * @var string
*/ */
protected $table = 'persons'; protected $table = 'persons';
/** /**
* @var string * @var string
*/ */
@ -98,209 +99,39 @@ class PersonModel extends Model
return $this->where('full_name', $fullName)->first(); return $this->where('full_name', $fullName)->first();
} }
public function addPerson(
string $fullName,
?string $informationUrl,
string $image
): int|bool {
$person = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $informationUrl,
'image' => new Image(download_file($image)),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
return $this->insert($person);
}
/** /**
* @return Person[] * @return stdClass[]
*/ */
public function getEpisodePersons(int $podcastId, int $episodeId): array public function getPersonRoles(int $personId, int $podcastId, ?int $episodeId): array {
{ if ($episodeId) {
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons"; $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}_roles";
if (!($found = cache($cacheName))) {
$found = $this->db
->table('episodes_persons')
->select('episodes_persons.*')
->where('episode_id', $episodeId)
->join('persons', 'person_id=persons.id')
->orderby('full_name')
->findAll();
cache()->save($cacheName, $found, DECADE); if (!($found = cache($cacheName))) {
$found = $this
->select('episodes_persons.person_group as group, episodes_persons.person_role as role')
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
->where('persons.id', $personId)
->where('episodes_persons.episode_id', $episodeId)
->get()
->getResultObject();
}
} else {
$cacheName = "podcast#{$podcastId}_person#{$personId}_roles";
if (!($found = cache($cacheName))) {
$found = $this
->select('podcasts_persons.person_group as group, podcasts_persons.person_role as role')
->join('podcasts_persons', 'persons.id = podcasts_persons.person_id')
->where('persons.id', $personId)
->where('podcasts_persons.podcast_id', $podcastId)
->get()
->getResultObject();
}
} }
return $found; return $found;
} }
/**
* @return Person[]
*/
public function getPodcastPersons(int $podcastId): array
{
$cacheName = "podcast#{$podcastId}_persons";
if (!($found = cache($cacheName))) {
$found = $this->db
->table('podcasts_persons')
->select('podcasts_persons.*')
->where('podcast_id', $podcastId)
->join('persons', 'person_id=persons.id')
->orderby('full_name')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function addEpisodePerson(
int $podcastId,
int $episodeId,
int $personId,
string $group,
string $role
): int|bool {
return $this->db->table('episodes_persons')->insert([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $group,
'person_role' => $role,
]);
}
public function addPodcastPerson(
int $podcastId,
int $personId,
string $group,
string $role
): int|bool {
return $this->db->table('podcasts_persons')->insert([
'podcast_id' => $podcastId,
'person_id' => $personId,
'person_group' => $group,
'person_role' => $role,
]);
}
/**
* Add persons to podcast
*
* @param array<string> $persons
* @param array<string, string> $groupsRoles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addPodcastPersons(
int $podcastId,
array $persons = [],
array $groupsRoles = []
): int|bool {
if ($persons === []) {
return 0;
}
$this->clearCache(['podcast_id' => $podcastId]);
$data = [];
foreach ($persons as $person) {
if ($groupsRoles === []) {
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
];
}
foreach ($groupsRoles 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],
];
}
}
return $this->insertBatch($data);
}
/**
* Add persons to episode
*
* @return BaseResult|bool Number of rows inserted or FALSE on failure
*/
public function removePodcastPersons(int $podcastId, int $personId): BaseResult|bool
{
return $this->delete([
'id' => $personId,
'podcast_id' => $podcastId,
]);
}
/**
* Add persons to episode
*
* @param int[] $personIds
* @param string[] $groups_roles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addEpisodePersons(
int $podcastId,
int $episodeId,
array $personIds,
array $groups_roles
): bool|int {
if (!empty($personIds)) {
$this->clearCache([
'episode_id' => $episodeId,
]);
$data = [];
foreach ($personIds as $personId) {
if ($groups_roles !== []) {
foreach ($groups_roles as $group_role) {
$group_role = explode(',', $group_role);
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $group_role[0],
'person_role' => $group_role[1],
];
}
} else {
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
];
}
}
return $this->insertBatch($data);
}
return 0;
}
/**
* @return BaseResult|bool
*/
public function removeEpisodePersons(
int $podcastId,
int $episodeId,
int $personId
): BaseResult|bool {
return $this->delete([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'id' => $personId,
]);
}
/** /**
* @return array<string, string> * @return array<string, string>
*/ */
@ -342,7 +173,7 @@ class PersonModel extends Model
foreach ($group['roles'] as $role_key => $role) { foreach ($group['roles'] as $role_key => $role) {
$options[ $options[
"{$group_key},{$role_key}" "{$group_key},{$role_key}"
] = "{$group['label']} {$role['label']}"; ] = "{$group['label']} {$role['label']}";
} }
} }
@ -352,6 +183,209 @@ class PersonModel extends Model
return $options; return $options;
} }
public function addPerson(
string $fullName,
?string $informationUrl,
string $image
): int|bool {
$person = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $informationUrl,
'image' => new Image(download_file($image)),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
return $this->insert($person);
}
/**
* @return Person[]
*/
public function getEpisodePersons(int $podcastId, int $episodeId): array
{
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
if (!($found = cache($cacheName))) {
$found = $this
->select('persons.*, episodes_persons.podcast_id, episodes_persons.episode_id')
->distinct()
->join('episodes_persons', 'persons.id = episodes_persons.person_id')
->where('episodes_persons.episode_id', $episodeId)
->orderby('persons.full_name')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return Person[]
*/
public function getPodcastPersons(int $podcastId): array
{
$cacheName = "podcast#{$podcastId}_persons";
if (!($found = cache($cacheName))) {
$found = $this
->select('persons.*, podcasts_persons.podcast_id as podcast_id')
->distinct()
->join('podcasts_persons', 'persons.id=podcasts_persons.person_id')
->where('podcasts_persons.podcast_id', $podcastId)
->orderby('persons.full_name')
->findAll();
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function addEpisodePerson(
int $podcastId,
int $episodeId,
int $personId,
string $groupSlug,
string $roleSlug
): int|bool {
return $this->db->table('episodes_persons')->insert([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $groupSlug,
'person_role' => $roleSlug,
]);
}
public function addPodcastPerson(
int $podcastId,
int $personId,
string $groupSlug,
string $roleSlug
): int|bool {
return $this->db->table('podcasts_persons')->insert([
'podcast_id' => $podcastId,
'person_id' => $personId,
'person_group' => $groupSlug,
'person_role' => $roleSlug,
]);
}
/**
* Add persons to podcast
*
* @param array<string> $persons
* @param array<string, string> $groupsRoles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addPodcastPersons(
int $podcastId,
array $persons = [],
array $roles = []
): int|bool {
if ($persons === []) {
return 0;
}
cache()->delete("podcast#{$podcastId}_persons");
(new PodcastModel())->clearCache(['id' => $podcastId]);
$data = [];
foreach ($persons as $person) {
if ($roles === []) {
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
];
}
foreach ($roles as $role) {
$groupRole = explode(',', $role);
$data[] = [
'podcast_id' => $podcastId,
'person_id' => $person,
'person_group' => $groupRole[0],
'person_role' => $groupRole[1],
];
}
}
return $this->db->table('podcasts_persons')->insertBatch($data);
}
/**
* Add persons to episode
*
* @return BaseResult|bool Number of rows inserted or FALSE on failure
*/
public function removePersonFromPodcast(int $podcastId, int $personId): BaseResult|bool
{
return $this->db->table('podcasts_persons')->delete([
'podcast_id' => $podcastId,
'person_id' => $personId,
]);
}
/**
* Add persons to episode
*
* @param int[] $personIds
* @param string[] $groupsRoles
*
* @return bool|int Number of rows inserted or FALSE on failure
*/
public function addEpisodePersons(
int $podcastId,
int $episodeId,
array $personIds,
array $groupsRoles
): bool|int {
if (!empty($personIds)) {
(new EpisodeModel())->clearCache(['id' => $episodeId]);
$data = [];
foreach ($personIds as $personId) {
if ($groupsRoles !== []) {
foreach ($groupsRoles as $groupRole) {
$groupRole = explode(',', $groupRole);
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
'person_group' => $groupRole[0],
'person_role' => $groupRole[1],
];
}
} else {
$data[] = [
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
];
}
}
return $this->db->table('episodes_persons')->insertBatch($data);
}
return 0;
}
/**
* @return BaseResult|bool
*/
public function removePersonFromEpisode(
int $podcastId,
int $episodeId,
int $personId
): BaseResult|bool {
return $this->db->table('episodes_persons')->delete([
'podcast_id' => $podcastId,
'episode_id' => $episodeId,
'person_id' => $personId,
]);
}
/** /**
* @param mixed[] $data * @param mixed[] $data
* *
@ -359,12 +393,10 @@ class PersonModel extends Model
*/ */
protected function clearCache(array $data): array protected function clearCache(array $data): array
{ {
$person = (new PersonModel())->find( $personId = is_array($data['id']) ? $data['id']['id'] : $data['id'];
is_array($data['id']) ? $data['id'][0] : $data['id'],
);
cache()->delete('person_options'); cache()->delete('person_options');
cache()->delete("person#{$person->id}"); cache()->delete("person#{$personId}");
// clear cache for every credits page // clear cache for every credits page
cache()->deleteMatching('page_credits_*'); cache()->deleteMatching('page_credits_*');

View File

@ -53,7 +53,7 @@ class PodcastModel extends Model
'is_locked', 'is_locked',
'location_name', 'location_name',
'location_geo', 'location_geo',
'location_osm_id', 'location_osm',
'payment_pointer', 'payment_pointer',
'custom_rss', 'custom_rss',
'partner_id', 'partner_id',
@ -218,11 +218,11 @@ class PodcastModel extends Model
->delete(); ->delete();
} }
public function getContributorGroupId(int $userId, int $podcastId): int|false public function getContributorGroupId(int $userId, int|string $podcastId): int|false
{ {
if (!is_numeric($podcastId)) { if (!is_numeric($podcastId)) {
// identifier is the podcast name, request must be a join // identifier is the podcast name, request must be a join
$user_podcast = $this->db $userPodcast = $this->db
->table('podcasts_users') ->table('podcasts_users')
->select('group_id, user_id') ->select('group_id, user_id')
->join('podcasts', 'podcasts.id = podcasts_users.podcast_id') ->join('podcasts', 'podcasts.id = podcasts_users.podcast_id')
@ -233,7 +233,7 @@ class PodcastModel extends Model
->get() ->get()
->getResultObject(); ->getResultObject();
} else { } else {
$user_podcast = $this->db $userPodcast = $this->db
->table('podcasts_users') ->table('podcasts_users')
->select('group_id') ->select('group_id')
->where([ ->where([
@ -244,8 +244,8 @@ class PodcastModel extends Model
->getResultObject(); ->getResultObject();
} }
return count($user_podcast) > 0 return count($userPodcast) > 0
? $user_podcast[0]->group_id ? $userPodcast[0]->group_id
: false; : false;
} }
@ -446,7 +446,7 @@ class PodcastModel extends Model
* *
* @return mixed[] * @return mixed[]
*/ */
protected function clearCache(array $data): array public function clearCache(array $data): array
{ {
$podcast = (new PodcastModel())->getPodcastById( $podcast = (new PodcastModel())->getPodcastById(
is_array($data['id']) ? $data['id'][0] : $data['id'], is_array($data['id']) ? $data['id'][0] : $data['id'],

View File

@ -5,7 +5,7 @@
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('pageTitle') ?> <?= $this->section('pageTitle') ?>
<?= lang('Person.episode_form.title') ?> (<?= count($episodePersons) ?>) <?= lang('Person.episode_form.title') ?> (<?= count($episode->persons) ?>)
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('headerRight') ?> <?= $this->section('headerRight') ?>
@ -25,7 +25,6 @@
]) ?> ]) ?>
<?= csrf_field() ?> <?= csrf_field() ?>
<?php if ($episodePersons): ?>
<?= form_section( <?= form_section(
lang('Person.episode_form.manage_section_title'), lang('Person.episode_form.manage_section_title'),
@ -37,42 +36,45 @@
[ [
[ [
'header' => lang('Person.episode_form.person'), 'header' => lang('Person.episode_form.person'),
'cell' => function ($episodePerson) { 'cell' => function ($person) {
return '<div class="flex">' . return '<div class="flex">' .
'<a href="' . '<a href="' .
route_to('person-view', $episodePerson->person->id) . route_to('person-view', $person->id) .
"\"><img src=\"{$episodePerson->person->image->thumbnail_url}\" alt=\"{$episodePerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" . "\"><img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' . '<div class="flex flex-col ml-3">' .
$episodePerson->person->full_name . $person->full_name .
($episodePerson->person_group && $episodePerson->person_role implode(
? '<span class="text-sm text-gray-600">' . '',
lang( array_map(function ($role) {
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label", return '<span class="text-sm text-gray-600">' .
) . lang(
' ▸ ' . "PersonsTaxonomy.persons.{$role->group}.label",
lang( ) .
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label", ' ' .
) . lang(
'</span>' "PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label",
: '') . ) .
(empty($episodePerson->person->information_url) '</span>';
}, $person->roles),
) .
($person->information_url === null
? '' ? ''
: "<a href=\"{$episodePerson->person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" . : "<a href=\"{$person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
$episodePerson->person->information_url . $person->information_url .
'</a>') . '</a>') .
'</div></div>'; '</div></div>';
}, },
], ],
[ [
'header' => lang('Common.actions'), 'header' => lang('Common.actions'),
'cell' => function ($episodePerson): string { 'cell' => function ($person): string {
return button( return button(
lang('Person.episode_form.remove'), lang('Person.episode_form.remove'),
route_to( route_to(
'episode-person-remove', 'episode-person-remove',
$episodePerson->podcast_id, $person->podcast_id,
$episodePerson->episode_id, $person->episode_id,
$episodePerson->id, $person->id,
), ),
[ [
'variant' => 'danger', 'variant' => 'danger',
@ -82,11 +84,10 @@
}, },
], ],
], ],
$episodePersons, $episode->persons,
) ?> ) ?>
<?= form_section_close() ?> <?= form_section_close() ?>
<?php endif; ?>
<?= form_section( <?= form_section(
@ -117,10 +118,7 @@
'person_group_role[]', 'person_group_role[]',
$taxonomyOptions, $taxonomyOptions,
old('person_group_role', []), old('person_group_role', []),
[ ['id' => 'person_group_role', 'class' => 'form-select mb-4'],
'id' => 'person_group_role',
'class' => 'form-select mb-4',
],
) ?> ) ?>

View File

@ -53,34 +53,40 @@
<tr> <tr>
<td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => "soundbites_array[{$soundbite->id}][start_time]", 'type' => 'number',
'name' => "soundbites_array[{$soundbite->id}][start_time]", 'min' => 0,
'max' => $episode->audio_file_duration,
'step' => 'any',
'id' => "soundbites[{$soundbite->id}][start_time]",
'name' => "soundbites[{$soundbite->id}][start_time]",
'class' => 'form-input w-full border-none text-center', 'class' => 'form-input w-full border-none text-center',
'value' => $soundbite->start_time, 'value' => $soundbite->start_time,
'data-type' => 'soundbite-field', 'data-type' => 'soundbite-field',
'data-field-type' => 'start-time', 'data-field-type' => 'start-time',
'data-soundbite-id' => $soundbite->id, 'data-soundbite-id' => $soundbite->id,
'required' => 'required', 'required' => 'required',
'min' => '0',
], ],
) ?></td> ) ?></td>
<td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => "soundbites_array[{$soundbite->id}][duration]", 'type' => 'number',
'name' => "soundbites_array[{$soundbite->id}][duration]", 'min' => 0,
'max' => $episode->audio_file_duration,
'step' => 'any',
'id' => "soundbites[{$soundbite->id}][duration]",
'name' => "soundbites[{$soundbite->id}][duration]",
'class' => 'form-input w-full border-none text-center', 'class' => 'form-input w-full border-none text-center',
'value' => $soundbite->duration, 'value' => $soundbite->duration,
'data-type' => 'soundbite-field', 'data-type' => 'soundbite-field',
'data-field-type' => 'duration', 'data-field-type' => 'duration',
'data-soundbite-id' => $soundbite->id, 'data-soundbite-id' => $soundbite->id,
'required' => 'required', 'required' => 'required',
'min' => '0',
], ],
) ?></td> ) ?></td>
<td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => "soundbites_array[{$soundbite->id}][label]", 'id' => "soundbites[{$soundbite->id}][label]",
'name' => "soundbites_array[{$soundbite->id}][label]", 'name' => "soundbites[{$soundbite->id}][label]",
'class' => 'form-input w-full border-none', 'class' => 'form-input w-full border-none',
'value' => $soundbite->label, 'value' => $soundbite->label,
], ],
@ -116,20 +122,27 @@
<tr> <tr>
<td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => 'soundbites_array[0][start_time]', 'type' => 'number',
'name' => 'soundbites_array[0][start_time]', 'min' => 0,
'max' => $episode->audio_file_duration,
'step' => 'any',
'id' => 'soundbites[0][start_time]',
'name' => 'soundbites[0][start_time]',
'class' => 'form-input w-full border-none text-center', 'class' => 'form-input w-full border-none text-center',
'value' => old('start_time'), 'value' => old('start_time'),
'data-soundbite-id' => '0', 'data-soundbite-id' => '0',
'data-type' => 'soundbite-field', 'data-type' => 'soundbite-field',
'data-field-type' => 'start-time', 'data-field-type' => 'start-time',
'min' => '0',
], ],
) ?></td> ) ?></td>
<td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => 'soundbites_array[0][duration]', 'type' => 'number',
'name' => 'soundbites_array[0][duration]', 'min' => 0,
'max' => $episode->audio_file_duration,
'step' => 'any',
'id' => 'soundbites[0][duration]',
'name' => 'soundbites[0][duration]',
'class' => 'form-input w-full border-none text-center', 'class' => 'form-input w-full border-none text-center',
'value' => old('duration'), 'value' => old('duration'),
'data-soundbite-id' => '0', 'data-soundbite-id' => '0',
@ -140,8 +153,8 @@
) ?></td> ) ?></td>
<td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input( <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
[ [
'id' => 'soundbites_array[0][label]', 'id' => 'soundbites[0][label]',
'name' => 'soundbites_array[0][label]', 'name' => 'soundbites[0][label]',
'class' => 'form-input w-full border-none', 'class' => 'form-input w-full border-none',
'value' => old('label'), 'value' => old('label'),
], ],
@ -149,7 +162,7 @@
<td class="px-4 py-2"><?= icon_button( <td class="px-4 py-2"><?= icon_button(
'play', 'play',
lang('Episode.soundbites_form.play'), lang('Episode.soundbites_form.play'),
null, '',
['variant' => 'primary'], ['variant' => 'primary'],
[ [
'data-type' => 'play-soundbite', 'data-type' => 'play-soundbite',
@ -170,13 +183,12 @@
</td><td class="px-4 py-2"><?= icon_button( </td><td class="px-4 py-2"><?= icon_button(
'timer', 'timer',
lang('Episode.soundbites_form.bookmark'), lang('Episode.soundbites_form.bookmark'),
null, '',
['variant' => 'info'], ['variant' => 'info'],
[ [
'data-type' => 'get-soundbite', 'data-type' => 'get-soundbite',
'data-start-time-field-name' => 'data-start-time-field-name' => 'soundbites[0][start_time]',
'soundbites_array[0][start_time]', 'data-duration-field-name' => 'soundbites[0][duration]',
'data-duration-field-name' => 'soundbites_array[0][duration]',
], ],
) ?></td></tr> ) ?></td></tr>
</tbody> </tbody>

View File

@ -1,135 +0,0 @@
<?= $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): string {
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'),
'',
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end'],
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,129 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Person.podcast_form.title') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Person.podcast_form.title') ?> (<?= count($podcast->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') ?>
<?= form_open(route_to('podcast-person-edit', $podcast->id), [
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?= 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 ($person) {
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $person->id) .
"\"><img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' .
$person->full_name .
implode(
'',
array_map(function ($role) {
return '<span class="text-sm text-gray-600">' .
lang(
"PersonsTaxonomy.persons.{$role->group}.label",
) .
' ' .
lang(
"PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label",
) .
'</span>';
}, $person->roles),
) .
($person->information_url === null
? ''
: "<a href=\"{$person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" .
$person->information_url .
'</a>') .
'</div></div>';
},
],
[
'header' => lang('Common.actions'),
'cell' => function ($person): string {
return button(
lang('Person.podcast_form.remove'),
route_to(
'podcast-person-remove',
$person->podcast_id,
$person->id,
),
[
'variant' => 'danger',
'size' => 'small',
],
);
},
],
],
$podcast->persons,
) ?>
<?= form_section_close() ?>
<?= 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('persons[]', $personOptions, old('persons', []), [
'id' => 'persons',
'class' => 'form-select mb-4',
'required' => 'required',
]) ?>
<?= form_label(
lang('Person.podcast_form.roles'),
'roles',
[],
lang('Person.podcast_form.roles_hint'),
true,
) ?>
<?= form_multiselect('roles[]', $taxonomyOptions, old('roles', []), [
'id' => 'roles',
'class' => 'form-select mb-4',
]) ?>
<?= form_section_close() ?>
<?= button(
lang('Person.podcast_form.submit_add'),
'',
['variant' => 'primary'],
['type' => 'submit', 'class' => 'self-end'],
) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -3,7 +3,7 @@
use Config\Services; use Config\Services;
use CodeIgniter\CodeIgniter; use CodeIgniter\CodeIgniter;
$error_id = uniqid('error', true); $errorId = uniqid('error', true);
?> ?>
<!doctype html> <!doctype html>
<html> <html>
@ -103,12 +103,12 @@ $error_id = uniqid('error', true);
$row['class'] . $row['type'] . $row['function'], $row['class'] . $row['type'] . $row['function'],
) ?> ) ?>
<?php if (!empty($row['args'])): ?> <?php if (!empty($row['args'])): ?>
<?php $args_id = $error_id . 'args' . $index; ?> <?php $argsId = $errorId . 'args' . $index; ?>
( <a href="#" onclick="return toggle('<?= esc( ( <a href="#" onclick="return toggle('<?= esc(
$args_id, $argsId,
'attr', 'attr',
) ?>');">arguments</a> ) ) ?>');">arguments</a> )
<div class="args" id="<?= esc($args_id, 'attr') ?>"> <div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0"> <table cellspacing="0">
<?php <?php

View File

@ -27,7 +27,7 @@
'text-2xl inline-flex items-baseline font-bold font-display', 'text-2xl inline-flex items-baseline font-bold font-display',
], ],
) ?> ) ?>
<?php if (user()->podcasts): ?> <?php if (user()->podcasts !== []): ?>
<button type="button" class="inline-flex items-center px-6 py-2 mt-auto font-semibold outline-none focus:ring" id="interact-as-dropdown" data-dropdown="button" data-dropdown-target="interact-as-dropdown-menu" aria-haspopup="true" aria-expanded="false"> <button type="button" class="inline-flex items-center px-6 py-2 mt-auto font-semibold outline-none focus:ring" id="interact-as-dropdown" data-dropdown="button" data-dropdown-target="interact-as-dropdown-menu" aria-haspopup="true" aria-expanded="false">
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" class="w-8 h-8 mr-2 rounded-full" /> ->avatar_image_url ?>" class="w-8 h-8 mr-2 rounded-full" />

View File

@ -42,37 +42,7 @@
</span> </span>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if (!empty($persons)): ?> <?= person_list($podcast->persons, 'mb-6') ?>
<div class="flex w-full mb-6 space-x-2 overflow-y-auto">
<?php foreach ($persons as $person): ?>
<?php if ($person['information_url']): ?>
<a href="<?= $person[
'information_url'
] ?>" target="_blank" rel="noreferrer noopener" class="flex-shrink-0">
<img
src="<?= $person['thumbnail_url'] ?>"
alt="<?= $person['full_name'] ?>"
class="object-cover w-12 h-12 rounded-full"
data-toggle="tooltip"
data-placement="bottom"
title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" />
</a>
<?php else: ?>
<img
src="<?= $person['thumbnail_url'] ?>"
alt="<?= $person['full_name'] ?>"
class="object-cover w-12 h-12 rounded-full"
data-toggle="tooltip"
data-placement="bottom"
title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" />
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="space-x-4"> <div class="space-x-4">
<a href="#" class="hover:underline"><?= lang('Podcast.followers', [ <a href="#" class="hover:underline"><?= lang('Podcast.followers', [
'numberOfFollowers' => $podcast->actor->followers_count, 'numberOfFollowers' => $podcast->actor->followers_count,

View File

@ -111,37 +111,8 @@
], ],
) ?> ) ?>
</div> </div>
<?php if ($episode->location !== null): ?> <?= location_link($episode->location, 'text-sm mb-4') ?>
<?= location_link($episode->location, 'text-sm mb-4') ?> <?= person_list($episode->persons) ?>
<?php endif; ?>
<?php if ($episodePersons): ?>
<div class="flex w-full space-x-2 overflow-y-auto">
<?php foreach ($episodePersons as $person): ?>
<?php if ($person['information_url']): ?>
<a href="<?= $person[
'information_url'
] ?>" target="_blank" rel="noreferrer noopener" class="flex-shrink-0">
<img src="<?= $person[
'thumbnail_url'
] ?>" alt="<?= $person[
'full_name'
] ?>" class="object-cover w-12 h-12 rounded-full" data-toggle="tooltip"
data-placement="bottom" title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" /></a>
<?php else: ?>
<img src="<?= $person[
'thumbnail_url'
] ?>" alt="<?= $person[
'full_name'
] ?>" class="object-cover w-12 h-12 rounded-full" data-toggle="tooltip"
data-placement="bottom" title="[<?= $person['full_name'] ?>] <?= $person[
'roles'
] ?>" />
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div> </div>
</div> </div>
<audio controls preload="none" class="w-full mt-auto"> <audio controls preload="none" class="w-full mt-auto">