feat: import subscriptions
All checks were successful
repod / xml (push) Successful in 21s
repod / php (push) Successful in 56s
repod / nodejs (push) Successful in 2m11s
repod / release (push) Has been skipped

This commit is contained in:
Michel Roux 2024-01-18 17:06:26 +01:00
parent c28abc7564
commit 751c3c1e01
5 changed files with 88 additions and 4 deletions

View File

@ -16,6 +16,7 @@ return [
['name' => 'episodes#action', 'url' => '/episodes/action', 'verb' => 'GET'], ['name' => 'episodes#action', 'url' => '/episodes/action', 'verb' => 'GET'],
['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'], ['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'],
['name' => 'opml#export', 'url' => '/opml/export', 'verb' => 'GET'], ['name' => 'opml#export', 'url' => '/opml/export', 'verb' => 'GET'],
['name' => 'opml#import', 'url' => '/opml/import', 'verb' => 'POST'],
['name' => 'podcast#index', 'url' => '/podcast', 'verb' => 'GET'], ['name' => 'podcast#index', 'url' => '/podcast', 'verb' => 'GET'],
['name' => 'search#index', 'url' => '/search', 'verb' => 'GET'], ['name' => 'search#index', 'url' => '/search', 'verb' => 'GET'],
['name' => 'tops#hot', 'url' => '/tops/hot', 'verb' => 'GET'], ['name' => 'tops#hot', 'url' => '/tops/hot', 'verb' => 'GET'],

View File

@ -30,9 +30,7 @@ class EpisodesController extends Controller
public function list(string $url): JSONResponse { public function list(string $url): JSONResponse {
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
$feed = $client->get($url); $feed = $client->get($url);
$episodes = $this->episodeActionReader->parseRssXml((string) $feed->getBody()); $episodes = $this->episodeActionReader->parseRssXml((string) $feed->getBody());
usort($episodes, fn (EpisodeActionExtraData $a, EpisodeActionExtraData $b) => $b->getPubDate() <=> $a->getPubDate()); usort($episodes, fn (EpisodeActionExtraData $a, EpisodeActionExtraData $b) => $b->getPubDate() <=> $a->getPubDate());
$episodes = array_values(array_intersect_key($episodes, array_unique(array_map(fn (EpisodeActionExtraData $episode) => $episode->getGuid(), $episodes)))); $episodes = array_values(array_intersect_key($episodes, array_unique(array_map(fn (EpisodeActionExtraData $episode) => $episode->getGuid(), $episodes))));

View File

@ -6,10 +6,12 @@ namespace OCA\RePod\Controller;
use OCA\GPodderSync\Core\PodcastData\PodcastDataReader; use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader; use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader;
use OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChangeSaver;
use OCA\RePod\AppInfo\Application; use OCA\RePod\AppInfo\Application;
use OCA\RePod\Service\UserService; use OCA\RePod\Service\UserService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\Response;
use OCP\IL10N; use OCP\IL10N;
use OCP\IRequest; use OCP\IRequest;
@ -20,6 +22,7 @@ class OpmlController extends Controller
private IL10N $l10n, private IL10N $l10n,
private PodcastDataReader $podcastDataReader, private PodcastDataReader $podcastDataReader,
private PodcastMetricsReader $podcastMetricsReader, private PodcastMetricsReader $podcastMetricsReader,
private SubscriptionChangeSaver $subscriptionChangeSaver,
private UserService $userService private UserService $userService
) { ) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
@ -54,6 +57,8 @@ class OpmlController extends Controller
$outline = $body->addChild('outline'); $outline = $body->addChild('outline');
if (isset($outline)) { if (isset($outline)) {
$outline->addAttribute('xmlUrl', $subscription->getUrl());
$title = $podcast->getTitle(); $title = $podcast->getTitle();
$link = $podcast->getLink(); $link = $podcast->getLink();
@ -63,7 +68,7 @@ class OpmlController extends Controller
} }
if ($link) { if ($link) {
$outline->addAttribute('xmlUrl', $link); $outline->addAttribute('htmlUrl', $link);
} }
} }
} }
@ -72,4 +77,28 @@ class OpmlController extends Controller
return new DataDownloadResponse((string) $xml->asXML(), 'repod-'.$dateCreated->getTimestamp().'.opml', ' application/xml'); return new DataDownloadResponse((string) $xml->asXML(), 'repod-'.$dateCreated->getTimestamp().'.opml', ' application/xml');
} }
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function import(): Response {
$file = $this->request->getUploadedFile('import');
if ($file) {
$xml = new \SimpleXMLElement(file_get_contents((string) $file['tmp_name']));
/** @var \SimpleXMLElement[] $outlines */
$outlines = $xml->body->children();
$toSubscribe = [];
foreach ($outlines as $outline) {
$toSubscribe[] = (string) $outline['xmlUrl'];
}
$this->subscriptionChangeSaver->saveSubscriptionChanges($toSubscribe, [], $this->userService->getUserUID());
}
return new Response();
}
} }

View File

@ -86,6 +86,7 @@ class FyydService implements IPodProvider
$langClient = $this->clientService->newClient(); $langClient = $this->clientService->newClient();
$langResponse = $langClient->get(self::BASE_URL.'feature/podcast/hot/languages'); $langResponse = $langClient->get(self::BASE_URL.'feature/podcast/hot/languages');
$langJson = (array) json_decode((string) $langResponse->getBody(), true, flags: JSON_THROW_ON_ERROR); $langJson = (array) json_decode((string) $langResponse->getBody(), true, flags: JSON_THROW_ON_ERROR);
if (array_key_exists('data', $langJson) && is_array($langJson['data'])) { if (array_key_exists('data', $langJson) && is_array($langJson['data'])) {
$language = in_array($userLang, $langJson['data']) ? $userLang : 'en'; $language = in_array($userLang, $langJson['data']) ? $userLang : 'en';
} }

View File

@ -16,6 +16,31 @@
</div> </div>
</template> </template>
</NcAppNavigationItem> </NcAppNavigationItem>
<NcAppNavigationItem :name="t('repod', 'Import subscriptions')" @click="importModal = true">
<template #icon>
<Import :size="20" />
</template>
<template #extra>
<NcModal v-if="importModal" @close="importModal = false">
<div class="importModal">
<h2>{{ t('repod', 'Import OPML file') }}</h2>
<form v-if="!importLoading"
id="import"
:action="generateUrl('/apps/repod/opml/import')"
enctype="multipart/form-data"
method="post"
@submit.prevent="importOpml">
<input accept="application/xml,.opml"
name="import"
required
type="file">
<input type="submit">
</form>
<Loading v-if="importLoading" />
</div>
</NcModal>
</template>
</NcAppNavigationItem>
<NcAppNavigationItem :href="generateUrl('/apps/repod/opml/export')" <NcAppNavigationItem :href="generateUrl('/apps/repod/opml/export')"
:name="t('repod', 'Export subscriptions')"> :name="t('repod', 'Export subscriptions')">
<template #icon> <template #icon>
@ -26,28 +51,40 @@
</template> </template>
<script> <script>
import { NcAppNavigationItem, NcAppNavigationSettings, NcCounterBubble } from '@nextcloud/vue' import { NcAppNavigationItem, NcAppNavigationSettings, NcCounterBubble, NcModal } from '@nextcloud/vue'
import Export from 'vue-material-design-icons/Export.vue' import Export from 'vue-material-design-icons/Export.vue'
import Import from 'vue-material-design-icons/Import.vue'
import Loading from '../Atoms/Loading.vue'
import Minus from 'vue-material-design-icons/Minus.vue' import Minus from 'vue-material-design-icons/Minus.vue'
import Plus from 'vue-material-design-icons/Plus.vue' import Plus from 'vue-material-design-icons/Plus.vue'
import Speedometer from 'vue-material-design-icons/Speedometer.vue' import Speedometer from 'vue-material-design-icons/Speedometer.vue'
import SpeedometerMedium from 'vue-material-design-icons/SpeedometerMedium.vue' import SpeedometerMedium from 'vue-material-design-icons/SpeedometerMedium.vue'
import SpeedometerSlow from 'vue-material-design-icons/SpeedometerSlow.vue' import SpeedometerSlow from 'vue-material-design-icons/SpeedometerSlow.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
export default { export default {
name: 'Settings', name: 'Settings',
components: { components: {
Export, Export,
Import,
Loading,
Minus, Minus,
NcAppNavigationItem, NcAppNavigationItem,
NcAppNavigationSettings, NcAppNavigationSettings,
NcCounterBubble, NcCounterBubble,
NcModal,
Plus, Plus,
Speedometer, Speedometer,
SpeedometerMedium, SpeedometerMedium,
SpeedometerSlow, SpeedometerSlow,
}, },
data() {
return {
importModal: false,
importLoading: false,
}
},
computed: { computed: {
player() { player() {
return this.$store.state.player return this.$store.state.player
@ -60,6 +97,17 @@ export default {
this.$store.dispatch('player/rate', this.player.rate + diff) this.$store.dispatch('player/rate', this.player.rate + diff)
} }
}, },
async importOpml(event) {
try {
const formData = new FormData(event.target)
this.importLoading = true
await axios.post(event.target.action, formData)
} catch (e) {
console.error(e)
} finally {
location.reload()
}
},
}, },
} }
</script> </script>
@ -75,6 +123,13 @@ export default {
gap: .5rem; gap: .5rem;
} }
.importModal {
align-items: center;
display: flex;
flex-direction: column;
margin: 2rem;
}
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }