feat: add update rss feed feature for podcasts to import their latest episodes

closes #183
This commit is contained in:
root 2022-06-15 10:27:13 +00:00 committed by Yassine Doghri
parent 6be5d12877
commit 5eb9dc168e
4 changed files with 245 additions and 0 deletions

View File

@ -130,6 +130,10 @@ $routes->group(
$routes->post('delete', 'PodcastController::attemptDelete/$1', [
'filter' => 'permission:podcasts-delete',
]);
$routes->get('update', 'PodcastImportController::updateImport/$1', [
'as' => 'podcast-update-feed',
'filter' => 'permission:podcasts-import',
]);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PodcastPersonController/$1', [

View File

@ -460,4 +460,228 @@ class PodcastImportController extends BaseController
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function updateImport(): RedirectResponse
{
if ($this->podcast->imported_feed_url === null) {
return redirect()
->back()
->with('error', lang('Podcast.messages.podcastNotImported'));
}
try {
ini_set('user_agent', 'Castopod/' . CP_VERSION);
$feed = simplexml_load_file($this->podcast->imported_feed_url);
} catch (ErrorException $errorException) {
return redirect()
->back()
->withInput()
->with('errors', [
$errorException->getMessage() .
': <a href="' .
$this->podcast->imported_feed_url .
'" rel="noreferrer noopener" target="_blank">' .
$this->podcast->imported_feed_url .
' ⎋</a>',
]);
}
$nsPodcast = $feed->channel[0]->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
);
if ((string) $nsPodcast->locked === 'yes') {
return redirect()
->back()
->withInput()
->with('errors', [lang('PodcastImport.lock_import')]);
}
$itemsCount = $feed->channel[0]->item->count();
$lastItem = $itemsCount;
$lastEpisode = (new EpisodeModel())->where('podcast_id', $this->podcast->id)
->orderBy('created_at', 'desc')
->first();
if ($lastEpisode !== null) {
for ($itemNumber = 0; $itemNumber < $itemsCount; ++$itemNumber) {
$item = $feed->channel[0]->item[$itemNumber];
if (property_exists(
$item,
'guid'
) && $item->guid !== null && $lastEpisode->guid === (string) $item->guid) {
$lastItem = $itemNumber;
break;
}
}
}
if ($lastItem === 0) {
return redirect()
->back()
->with('message', lang('Podcast.messages.podcastFeedUpToDate'));
}
helper(['media', 'misc']);
$converter = new HtmlConverter();
$slugs = [];
$db = db_connect();
$db->transStart();
for ($itemNumber = 1; $itemNumber <= $lastItem; ++$itemNumber) {
$item = $feed->channel[0]->item[$lastItem - $itemNumber];
$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',
);
$textToSlugify = (string) $item->title;
$slug = slugify($textToSlugify, 120);
if (in_array($slug, $slugs, true) || (new EpisodeModel())->where([
'slug' => $slug,
'podcast_id' => $this->podcast->id,
])->first()) {
$slugNumber = 2;
while (in_array($slug . '-' . $slugNumber, $slugs, true) || (new EpisodeModel())->where([
'slug' => $slug . '-' . $slugNumber,
'podcast_id' => $this->podcast->id,
])->first()) {
++$slugNumber;
}
$slug = $slug . '-' . $slugNumber;
}
$slugs[] = $slug;
$itemDescriptionHtml = (string) $item->description;
if (
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$episodeCover = download_file((string) $nsItunes->image->attributes()['href']);
} else {
$episodeCover = null;
}
$location = null;
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
$location = new Location(
(string) $nsPodcast->location,
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
);
}
$newEpisode = new Episode([
'podcast_id' => $this->podcast->id,
'title' => $item->title,
'slug' => $slug,
'guid' => $item->guid ?? null,
'audio' => download_file(
(string) $item->enclosure->attributes()['url'],
(string) $item->enclosure->attributes()['type']
),
'description_markdown' => $converter->convert($itemDescriptionHtml),
'description_html' => $itemDescriptionHtml,
'cover' => $episodeCover,
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
? 'explicit'
: (in_array((string) $nsItunes->explicit, ['no', 'false'], true)
? 'clean'
: null))
: null,
'number' => ((string) $nsItunes->episode === '' ? null : (int) $nsItunes->episode),
'season_number' => ((string) $nsItunes->season === '' ? null : (int) $nsItunes->season),
'type' => property_exists($nsItunes, 'episodeType') && $nsItunes->episodeType !== null
? (string) $nsItunes->episodeType
: 'full',
'is_blocked' => property_exists(
$nsItunes,
'block'
) && $nsItunes->block !== null && (string) $nsItunes->block === 'yes',
'location' => $location,
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => strtotime((string) $item->pubDate),
]);
$episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
foreach ($nsPodcast->person as $episodePerson) {
$fullName = (string) $episodePerson;
$personModel = new PersonModel();
$newPersonId = null;
if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id;
} else {
$newPerson = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $episodePerson->attributes()['href'],
'avatar' => download_file((string) $episodePerson->attributes()['img']),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
if (! ($newPersonId = $personModel->insert($newPerson))) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
// TODO: these checks should be in the taxonomy as default values
$episodePersonGroup = $episodePerson->attributes()['group'] ?? 'Cast';
$episodePersonRole = $episodePerson->attributes()['role'] ?? 'Host';
$personGroup = ReversedTaxonomy::$taxonomy[(string) $episodePersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $episodePersonRole]['slug'];
$episodePersonModel = new PersonModel();
if (! $episodePersonModel->addEpisodePerson(
$this->podcast->id,
$newEpisodeId,
$newPersonId,
$personGroupSlug,
$personRoleSlug
)) {
return redirect()
->back()
->withInput()
->with('errors', $episodePersonModel->errors());
}
}
}
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id])->with(
'message',
lang('Podcast.messages.podcastFeedUpdateSuccess', [
'number_of_new_episodes' => $lastItem,
])
);
}
}

View File

@ -40,6 +40,12 @@ return [
other {media}
}.',
'deletePodcastMediaFolderError' => 'Failed to delete podcast media folder {folder_path}. You may manually remove it from your disk.',
'podcastFeedUpdateSuccess' => 'Successful update : {number_of_new_episodes, plural,
one {# episode was}
other {# episodes were}
} added to the podcast!',
'podcastFeedUpToDate' => 'This podcast is up to date.',
'podcastNotImported' => 'This podcast could not be updated as it was not imported.',
],
'form' => [
'identity_section_title' => 'Podcast identity',
@ -104,6 +110,9 @@ return [
'custom_rss_hint' => 'This will be injected within the ❬channel❭ tag.',
'new_feed_url' => 'New feed URL',
'new_feed_url_hint' => 'Use this field when you move to another domain or podcast hosting platform. By default, the value is set to the current RSS URL if the podcast is imported.',
'old_feed_url' => 'Old feed URL',
'update_feed' => 'Update feed',
'update_feed_tip' => 'Import this podcast\'s latest episodes',
'partnership' => 'Partnership',
'partner_id' => 'ID',
'partner_link_url' => 'Link URL',

View File

@ -229,6 +229,14 @@
value="<?= esc($podcast->new_feed_url) ?>"
/>
<?php if ($podcast->imported_feed_url !== null): ?>
<div class="flex flex-col">
<Forms.Label for="old_feed_url"><?= lang('Podcast.form.old_feed_url') ?></Forms.Label>
<Forms.Input name="old_feed_url" readonly="true" value="<?= esc($podcast->imported_feed_url) ?>" />
</div>
<Button variant="primary" class="self-end" uri="<?= route_to('podcast-update-feed', $podcast->id) ?>" iconLeft="refresh" data-tooltip="bottom" title="<?= lang('Podcast.form.update_feed_tip') ?>"><?= lang('Podcast.form.update_feed') ?></Button>
<?php endif ?>
</Forms.Section>
<Forms.Section