mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-05 17:02:01 +00:00
feat: add update rss feed feature for podcasts to import their latest episodes
closes #183
This commit is contained in:
parent
6be5d12877
commit
5eb9dc168e
@ -130,6 +130,10 @@ $routes->group(
|
|||||||
$routes->post('delete', 'PodcastController::attemptDelete/$1', [
|
$routes->post('delete', 'PodcastController::attemptDelete/$1', [
|
||||||
'filter' => 'permission:podcasts-delete',
|
'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->group('persons', function ($routes): void {
|
||||||
$routes->get('/', 'PodcastPersonController/$1', [
|
$routes->get('/', 'PodcastPersonController/$1', [
|
||||||
|
@ -460,4 +460,228 @@ class PodcastImportController extends BaseController
|
|||||||
|
|
||||||
return redirect()->route('podcast-view', [$newPodcastId]);
|
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,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,12 @@ return [
|
|||||||
other {media}
|
other {media}
|
||||||
}.',
|
}.',
|
||||||
'deletePodcastMediaFolderError' => 'Failed to delete podcast media folder {folder_path}. You may manually remove it from your disk.',
|
'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' => [
|
'form' => [
|
||||||
'identity_section_title' => 'Podcast identity',
|
'identity_section_title' => 'Podcast identity',
|
||||||
@ -104,6 +110,9 @@ return [
|
|||||||
'custom_rss_hint' => 'This will be injected within the ❬channel❭ tag.',
|
'custom_rss_hint' => 'This will be injected within the ❬channel❭ tag.',
|
||||||
'new_feed_url' => 'New feed URL',
|
'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.',
|
'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',
|
'partnership' => 'Partnership',
|
||||||
'partner_id' => 'ID',
|
'partner_id' => 'ID',
|
||||||
'partner_link_url' => 'Link URL',
|
'partner_link_url' => 'Link URL',
|
||||||
|
@ -229,6 +229,14 @@
|
|||||||
value="<?= esc($podcast->new_feed_url) ?>"
|
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>
|
||||||
|
|
||||||
<Forms.Section
|
<Forms.Section
|
||||||
|
Loading…
x
Reference in New Issue
Block a user