feat(rss): add podcast:location tag

This commit is contained in:
Benjamin Bellamy 2020-12-23 14:11:38 +00:00
parent ba088649d2
commit c0a22829bd
25 changed files with 421 additions and 15 deletions

View File

@ -126,6 +126,7 @@ class Episode extends BaseController
'enclosure' => $this->request->getFile('enclosure'),
'description_markdown' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'),
'location' => $this->request->getPost('location_name'),
'transcript' => $this->request->getFile('transcript'),
'chapters' => $this->request->getFile('chapters'),
'parental_advisory' =>
@ -222,6 +223,7 @@ class Episode extends BaseController
$this->episode->description_markdown = $this->request->getPost(
'description'
);
$this->episode->location = $this->request->getPost('location_name');
$this->episode->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')

View File

@ -161,6 +161,7 @@ class Podcast extends BaseController
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name'),
'payment_pointer' => $this->request->getPost('payment_pointer'),
'is_blocked' => $this->request->getPost('is_blocked') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes',
@ -254,6 +255,7 @@ class Podcast extends BaseController
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name');
$this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer'
);

View File

@ -121,11 +121,13 @@ class PodcastImport extends BaseController
$channelDescriptionHtml
),
'description_html' => $channelDescriptionHtml,
'image' => $nsItunes->image && !empty($nsItunes->image->attributes())
? download_file($nsItunes->image->attributes())
: ($feed->channel[0]->image && !empty($feed->channel[0]->image->url)
? download_file($feed->channel[0]->image->url)
: null),
'image' =>
$nsItunes->image && !empty($nsItunes->image->attributes())
? download_file($nsItunes->image->attributes())
: ($feed->channel[0]->image &&
!empty($feed->channel[0]->image->url)
? download_file($feed->channel[0]->image->url)
: null),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' => empty($nsItunes->explicit)
@ -146,6 +148,19 @@ class PodcastImport extends BaseController
'is_completed' => empty($nsItunes->complete)
? false
: $nsItunes->complete === 'yes',
'location_name' => !$nsPodcast->location
? null
: $nsPodcast->location->attributes()['name'],
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
? null
: $nsPodcast->location->attributes()['geo'],
'location_osmid' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['osmid'])
? null
: $nsPodcast->location->attributes()['osmid'],
'created_by' => user(),
'updated_by' => user(),
]);
@ -243,6 +258,9 @@ class PodcastImport extends BaseController
$nsItunes = $item->children(
'http://www.itunes.com/dtds/podcast-1.0.dtd'
);
$nsPodcast = $item->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
);
$slug = slugify(
$this->request->getPost('slug_field') === 'title'
@ -306,6 +324,19 @@ class PodcastImport extends BaseController
'is_blocked' => empty($nsItunes->block)
? false
: $nsItunes->block === 'yes',
'location_name' => !$nsPodcast->location
? null
: $nsPodcast->location->attributes()['name'],
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
? null
: $nsPodcast->location->attributes()['geo'],
'location_osmid' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['osmid'])
? null
: $nsPodcast->location->attributes()['osmid'],
'created_by' => user(),
'updated_by' => user(),
'published_at' => strtotime($item->pubDate),

View File

@ -123,6 +123,21 @@ class AddPodcasts extends Migration
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osmid' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,

View File

@ -109,6 +109,21 @@ class AddEpisodes extends Migration
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osmid' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,

View File

@ -120,6 +120,9 @@ class Episode extends Entity
'season_number' => '?integer',
'type' => 'string',
'is_blocked' => 'boolean',
'location_name' => '?string',
'location_geo' => '?string',
'location_osmid' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
];
@ -479,4 +482,32 @@ class Episode extends Entity
return 'scheduled';
}
/**
* Saves the location name and fetches OpenStreetMap info
*
* @param string $locationName
*
*/
public function setLocation($locationName = null)
{
helper('location');
if (
$locationName &&
(empty($this->attributes['location_name']) ||
$this->attributes['location_name'] != $locationName)
) {
$this->attributes['location_name'] = $locationName;
if ($location = fetch_osm_location($locationName)) {
$this->attributes['location_geo'] = $location['geo'];
$this->attributes['location_osmid'] = $location['osmid'];
}
} elseif (empty($locationName)) {
$this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null;
$this->attributes['location_osmid'] = null;
}
return $this;
}
}

View File

@ -96,6 +96,9 @@ class Podcast extends Entity
'is_locked' => 'boolean',
'imported_feed_url' => '?string',
'new_feed_url' => '?string',
'location_name' => '?string',
'location_geo' => '?string',
'location_osmid' => '?string',
'payment_pointer' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
@ -367,4 +370,32 @@ class Podcast extends Entity
return $this->other_categories_ids;
}
/**
* Saves the location name and fetches OpenStreetMap info
*
* @param string $locationName
*
*/
public function setLocation($locationName = null)
{
helper('location');
if (
$locationName &&
(empty($this->attributes['location_name']) ||
$this->attributes['location_name'] != $locationName)
) {
$this->attributes['location_name'] = $locationName;
if ($location = fetch_osm_location($locationName)) {
$this->attributes['location_geo'] = $location['geo'];
$this->attributes['location_osmid'] = $location['osmid'];
}
} elseif (empty($locationName)) {
$this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null;
$this->attributes['location_osmid'] = null;
}
return $this;
}
}

View File

@ -318,7 +318,7 @@ if (!function_exists('episode_numbering')) {
* @param string $class styling classes
* @param string $is_abbr component will show abbreviated numbering if true
*
* @return string
* @return string|null
*/
function episode_numbering(
$episodeNumber = null,
@ -368,4 +368,61 @@ if (!function_exists('episode_numbering')) {
}
}
if (!function_exists('location_link')) {
/**
* Returns link to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
*/
function location_link(
$locationName,
$locationGeo,
$locationOsmid,
$class = ''
) {
$link = null;
if (!empty($locationName)) {
$uri = '';
if (!empty($locationOsmid)) {
$uri =
'https://www.openstreetmap.org/' .
['N' => 'node', 'W' => 'way', 'R' => 'relation'][
substr($locationOsmid, 0, 1)
] .
'/' .
substr($locationOsmid, 1);
} elseif (!empty($locationGeo)) {
$uri =
'https://www.openstreetmap.org/#map=17/' .
str_replace(',', '/', substr($locationGeo, 4));
} else {
$uri =
'https://www.openstreetmap.org/search?query=' .
urlencode($locationName);
}
$link = button(
$locationName,
$uri,
[
'variant' => 'default',
'size' => 'small',
'isRoundedFull' => true,
'iconLeft' => 'map-pin',
],
[
'class' =>
'text-gray-800' . (empty($class) ? '' : " $class"),
'target' => '_blank',
'rel' => 'noreferrer noopener',
]
);
}
return $link;
}
}
// ------------------------------------------------------------------------

View File

@ -0,0 +1,52 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
/**
* Fetches places from Nominatim OpenStreetMap
*
* @param string $locationName
*
* @return array|null
*/
function fetch_osm_location($locationName)
{
$osmObject = null;
if (!empty($locationName)) {
try {
$client = \Config\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);
$osmObject = [
'geo' =>
empty($places[0]['lat']) || empty($places[0]['lon'])
? null
: "geo:{$places[0]['lat']},{$places[0]['lon']}",
'osmid' => empty($places[0]['osm_type'])
? null
: strtoupper(substr($places[0]['osm_type'], 0, 1)) .
$places[0]['osm_id'],
];
} catch (\Exception $e) {
//If things go wrong the show must go on
log_message('critical', $e);
}
}
return $osmObject;
}

View File

@ -65,7 +65,23 @@ function get_rss_feed($podcast, $serviceSlug = '')
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
$itunes_image->addAttribute('href', $podcast->image->original_url);
$channel->addChild('language', $podcast->language_code);
if (!empty($podcast->location_name)) {
$locationElement = $channel->addChild(
'location',
null,
$podcast_namespace
);
$locationElement->addAttribute(
'name',
htmlspecialchars($podcast->location_name)
);
if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $podcast->location_geo);
}
if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osmid', $podcast->location_osmid);
}
}
if (!empty($podcast->payment_pointer)) {
$valueElement = $channel->addChild('value', null, $podcast_namespace);
$valueElement->addAttribute('type', 'webmonetization');
@ -203,6 +219,26 @@ function get_rss_feed($podcast, $serviceSlug = '')
'pubDate',
$episode->published_at->format(DATE_RFC1123)
);
if (!empty($episode->location_name)) {
$locationElement = $item->addChild(
'location',
null,
$podcast_namespace
);
$locationElement->addAttribute(
'name',
htmlspecialchars($episode->location_name)
);
if (!empty($episode->location_geo)) {
$locationElement->addAttribute('geo', $episode->location_geo);
}
if (!empty($episode->location_osmid)) {
$locationElement->addAttribute(
'osmid',
$episode->location_osmid
);
}
}
$item->addChildWithCDATA('description', $episode->description_html);
$item->addChild(
'duration',

View File

@ -83,6 +83,10 @@ return [
'chapters' => 'Chapters',
'chapters_hint' => 'File should be in JSON Chapters Format.',
'chapters_delete' => 'Delete chapters',
'location_section_title' => 'Location',
'location_section_subtitle' => 'What place is this episode about?',
'location_name' => 'Location name or address',
'location_name_hint' => 'This can be a real place or fictional',
'submit_create' => 'Create episode',
'submit_edit' => 'Save episode',
],

View File

@ -61,6 +61,10 @@ return [
'publisher_hint' =>
'The group responsible for creating the show. Often refers to the parent company or network of a podcast. This field is sometimes labeled as Author.',
'copyright' => 'Copyright',
'location_section_title' => 'Location',
'location_section_subtitle' => 'What place is this podcast about?',
'location_name' => 'Location name or address',
'location_name_hint' => 'This can be a real place or fictional',
'monetization_section_title' => 'Monetization',
'monetization_section_subtitle' =>
'Earn money thanks to your audience.',

View File

@ -83,7 +83,11 @@ return [
'transcript_delete' => 'Supprimer la transcription',
'chapters' => 'Chapitrage',
'chapters_hint' => 'Le fichier doit être en "JSON Chapters Format".',
'chapters_delete' => 'Supprimer le chaptrage',
'chapters_delete' => 'Supprimer le chapitrage',
'location_section_title' => 'Localisation',
'location_section_subtitle' => 'De quel lieu cet épisode parle-t-il?',
'location_name' => 'Nom ou adresse du lieu',
'location_name_hint' => 'Ce lieu peut être réel ou fictif',
'submit_create' => 'Créer lépisode',
'submit_edit' => 'Enregistrer lépisode',
],

View File

@ -62,6 +62,10 @@ return [
'publisher_hint' =>
'Le groupe responsable de la création du podcast. Fait souvent référence à la société mère ou au réseau dun podcast. Ce champ est parfois appelé «Auteur».',
'copyright' => 'Droit dauteur',
'location_section_title' => 'Localisation',
'location_section_subtitle' => 'De quel lieu ce podcast parle-t-il?',
'location_name' => 'Nom ou adresse du lieu',
'location_name_hint' => 'Ce lieu peut être réel ou fictif',
'monetization_section_title' => 'Monétisation',
'monetization_section_subtitle' =>
'Gagnez de largent grâce à votre audience.',

View File

@ -35,6 +35,9 @@ class EpisodeModel extends Model
'season_number',
'type',
'is_blocked',
'location_name',
'location_geo',
'location_osmid',
'published_at',
'created_by',
'updated_by',

View File

@ -37,6 +37,9 @@ class PodcastModel extends Model
'is_blocked',
'is_completed',
'is_locked',
'location_name',
'location_geo',
'location_osmid',
'payment_pointer',
'created_by',
'updated_by',

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17.657 15.657L12 21.314l-5.657-5.657a8 8 0 1 1 11.314 0zM5 22h14v2H5v-2z"/></svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@ -187,6 +187,25 @@
<?= form_section_close() ?>
<?= form_section(
lang('Episode.form.location_section_title'),
lang('Episode.form.location_section_subtitle')
) ?>
<?= form_label(
lang('Episode.form.location_name'),
'location_name',
[],
lang('Episode.form.location_name_hint'),
true
) ?>
<?= form_input([
'id' => 'location_name',
'name' => 'location_name',
'class' => 'form-input mb-4',
'value' => old('location_name'),
]) ?>
<?= form_section_close() ?>
<?= form_section(
lang('Episode.form.publication_section_title'),

View File

@ -190,6 +190,25 @@
<?= form_section_close() ?>
<?= form_section(
lang('Episode.form.location_section_title'),
lang('Episode.form.location_section_subtitle')
) ?>
<?= form_label(
lang('Episode.form.location_name'),
'location_name',
[],
lang('Episode.form.location_name_hint'),
true
) ?>
<?= form_input([
'id' => 'location_name',
'name' => 'location_name',
'class' => 'form-input mb-4',
'value' => old('location_name', $episode->location_name),
]) ?>
<?= form_section_close() ?>
<?= form_section(
lang('Episode.form.publication_section_title'),

View File

@ -11,6 +11,12 @@
$episode->publication_status,
'text-sm ml-2 align-middle'
) ?>
<?= location_link(
$episode->location_name,
$episode->location_geo,
$episode->location_osmid,
'ml-2'
) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View File

@ -27,6 +27,7 @@
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'required' => 'required',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
@ -58,21 +59,27 @@
'required' => 'required',
]) ?>
<?= form_fieldset('', [
'class' => 'mb-4',
]) ?>
<?= form_fieldset('', ['class' => 'mb-4']) ?>
<legend>
<?= lang('Podcast.form.type.label') .
hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?>
</legend>
<?= form_radio(
['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'],
[
'id' => 'episodic',
'name' => 'type',
'class' => 'form-radio-btn',
],
'episodic',
old('type') ? old('type') == 'episodic' : true
) ?>
<label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label>
<?= form_radio(
['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'],
[
'id' => 'serial',
'name' => 'type',
'class' => 'form-radio-btn',
],
'serial',
old('type') ? old('type') == 'serial' : false
) ?>
@ -241,6 +248,26 @@
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.location_section_title'),
lang('Podcast.form.location_section_subtitle')
) ?>
<?= form_label(
lang('Podcast.form.location_name'),
'location_name',
[],
lang('Podcast.form.location_name_hint'),
true
) ?>
<?= form_input([
'id' => 'location_name',
'name' => 'location_name',
'class' => 'form-input mb-4',
'value' => old('location_name'),
]) ?>
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.monetization_section_title'),
lang('Podcast.form.monetization_section_subtitle')
@ -250,7 +277,8 @@
lang('Podcast.form.payment_pointer'),
'payment_pointer',
[],
lang('Podcast.form.payment_pointer_hint')
lang('Podcast.form.payment_pointer_hint'),
true
) ?>
<?= form_input([
'id' => 'payment_pointer',

View File

@ -251,6 +251,26 @@
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.location_section_title'),
lang('Podcast.form.location_section_subtitle')
) ?>
<?= form_label(
lang('Podcast.form.location_name'),
'location_name',
[],
lang('Podcast.form.location_name_hint'),
true
) ?>
<?= form_input([
'id' => 'location_name',
'name' => 'location_name',
'class' => 'form-input mb-4',
'value' => old('location_name', $podcast->location_name),
]) ?>
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.monetization_section_title'),
lang('Podcast.form.monetization_section_subtitle')
@ -260,7 +280,8 @@
lang('Podcast.form.payment_pointer'),
'payment_pointer',
[],
lang('Podcast.form.payment_pointer_hint')
lang('Podcast.form.payment_pointer_hint'),
true
) ?>
<?= form_input([
'id' => 'payment_pointer',

View File

@ -6,6 +6,12 @@
<?= $this->section('pageTitle') ?>
<?= $podcast->title ?>
<?= location_link(
$podcast->location_name,
$podcast->location_geo,
$podcast->location_osmid,
'ml-4'
) ?>
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>

View File

@ -100,6 +100,12 @@
<?= format_duration($episode->enclosure_duration) ?>
</time>
</div>
<?= location_link(
$episode->location_name,
$episode->location_geo,
$episode->location_osmid,
'self-start mt-2'
) ?>
<audio controls preload="none" class="w-full mt-auto">
<source src="<?= $episode->enclosure_web_url ?>" type="<?= $episode->enclosure_type ?>">
Your browser does not support the audio tag.

View File

@ -49,6 +49,12 @@
lang('Common.explicit') .
'</span>'
: '' ?>
<?= location_link(
$podcast->location_name,
$podcast->location_geo,
$podcast->location_osmid,
'ml-4'
) ?>
</div>
<div class="flex mb-2 space-x-2">
<?= anchor(