Commit 5eb9dc16 authored by Ola Hneini's avatar Ola Hneini Committed by Yassine Doghri
Browse files

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

closes #183
parent 6be5d128
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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', [
+224 −0
Original line number Diff line number Diff line
@@ -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,
            ])
        );
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -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',
+8 −0
Original line number Diff line number Diff line
@@ -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