Refacto API to be pseudo compatible with gpodder
All checks were successful
repod / nextcloud (push) Successful in 1m3s
repod / nodejs (push) Successful in 1m20s

This commit is contained in:
Michel Roux 2023-08-01 23:18:37 +02:00
parent 6d6f8b4cf7
commit 098db9f09e
15 changed files with 5101 additions and 1377 deletions

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
return [ return [
'routes' => [ 'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'top#index', 'url' => '/top', 'verb' => 'GET'], ['name' => 'toplist#index', 'url' => '/toplist/{count}', 'verb' => 'GET'],
['name' => 'search#index', 'url' => '/search/{value}', 'verb' => 'GET'], ['name' => 'search#index', 'url' => '/search/{value}', 'verb' => 'GET'],
], ],
]; ];

View File

@ -5,7 +5,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require-dev": { "require-dev": {
"nextcloud/ocp": "^27.0.1", "nextcloud/ocp": "^27.0.1",
"psalm/phar": "^5.13.1", "psalm/phar": "^5.14.1",
"nextcloud/coding-standard": "^1.1.1" "nextcloud/coding-standard": "^1.1.1"
}, },
"scripts": { "scripts": {

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a5d69457fcb9b9b73dd75bcd2b7c9791", "content-hash": "817269c30922145fbaf4270b2cb22d8e",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {

View File

@ -36,8 +36,8 @@ class SearchController extends Controller
} }
} }
usort($podcasts, fn (array $a, array $b) => new \DateTime((string) $b['last_pub']) <=> new \DateTime((string) $a['last_pub'])); usort($podcasts, fn (array $a, array $b) => new \DateTime((string) $b['lastpub']) <=> new \DateTime((string) $a['lastpub']));
$podcasts = array_intersect_key($podcasts, array_unique(array_map(fn (array $feed) => $feed['feed_url'], $podcasts))); $podcasts = array_intersect_key($podcasts, array_unique(array_map(fn (array $feed) => $feed['url'], $podcasts)));
return new JSONResponse($podcasts); return new JSONResponse($podcasts);
} }

View File

@ -11,7 +11,7 @@ use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest; use OCP\IRequest;
class TopController extends Controller class ToplistController extends Controller
{ {
public function __construct( public function __construct(
IRequest $request, IRequest $request,
@ -27,10 +27,7 @@ class TopController extends Controller
public function index(): JSONResponse public function index(): JSONResponse
{ {
try { try {
$response = $this->fyydService->hot(); return new JSONResponse($this->fyydService->hot());
$json = (array) json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
return new JSONResponse($json, $response->getStatusCode());
} catch (\Exception $e) { } catch (\Exception $e) {
return new JSONResponse([$e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); return new JSONResponse([$e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
} }

View File

@ -5,11 +5,13 @@ declare(strict_types=1);
namespace OCA\RePod\Service; namespace OCA\RePod\Service;
use OCP\Http\Client\IClientService; use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/**
* @psalm-import-type Podcast from IProvider
*/
class FyydService implements IProvider class FyydService implements IProvider
{ {
private const BASE_URL = 'https://api.fyyd.de/0.2/'; private const BASE_URL = 'https://api.fyyd.de/0.2/';
@ -41,15 +43,18 @@ class FyydService implements IProvider
/** @var string[] $feed */ /** @var string[] $feed */
foreach ($json['data'] as $feed) { foreach ($json['data'] as $feed) {
$podcasts[] = [ $podcasts[] = [
'provider' => 'fyyd',
'id' => $feed['id'], 'id' => $feed['id'],
'provider' => 'fyyd',
'website' => $feed['htmlURL'],
'description' => $feed['description'],
'title' => $feed['title'], 'title' => $feed['title'],
'author' => $feed['author'], 'author' => $feed['author'],
'image' => $feed['imgURL'], 'url' => $feed['xmlURL'],
'provider_url' => $feed['htmlURL'], 'position_last_week' => $feed['rank'],
'feed_url' => $feed['xmlURL'], 'mygpo_link' => $feed['url_fyyd'],
'last_pub' => $feed['lastpub'], 'logo_url' => $feed['imgURL'],
'nb_episodes' => $feed['episode_count'], 'lastpub' => $feed['lastpub'],
'episode_count' => $feed['episode_count'],
]; ];
} }
} }
@ -57,8 +62,12 @@ class FyydService implements IProvider
return $podcasts; return $podcasts;
} }
public function hot(): IResponse /**
* @return Podcast[]
*/
public function hot(int $count = 10): array
{ {
$podcasts = [];
$language = 'en'; $language = 'en';
$userLang = $this->userService->getLangCode(); $userLang = $this->userService->getLangCode();
@ -75,10 +84,34 @@ class FyydService implements IProvider
$podcastClient = $this->clientService->newClient(); $podcastClient = $this->clientService->newClient();
return $podcastClient->get(self::BASE_URL.'feature/podcast/hot', [ $podcastResponse = $podcastClient->get(self::BASE_URL.'feature/podcast/hot', [
'query' => [ 'query' => [
'count' => $count,
'language' => $language, 'language' => $language,
], ],
]); ]);
$postCastJson = (array) json_decode((string) $podcastResponse->getBody(), true, flags: JSON_THROW_ON_ERROR);
if (array_key_exists('data', $postCastJson) && is_array($postCastJson['data'])) {
/** @var string[] $feed */
foreach ($postCastJson['data'] as $feed) {
$podcasts[] = [
'id' => $feed['id'],
'provider' => 'fyyd',
'website' => $feed['htmlURL'],
'description' => $feed['description'],
'title' => $feed['title'],
'author' => $feed['author'],
'url' => $feed['xmlURL'],
'position_last_week' => $feed['rank'],
'mygpo_link' => $feed['url_fyyd'],
'logo_url' => $feed['imgURL'],
'lastpub' => $feed['lastpub'],
'episode_count' => $feed['episode_count'],
];
}
}
return $podcasts;
} }
} }

View File

@ -4,20 +4,26 @@ declare(strict_types=1);
namespace OCA\RePod\Service; namespace OCA\RePod\Service;
/**
* @psalm-type Podcast = array{
* id: string,
* provider: string,
* website: string,
* description: string,
* title: string,
* author: string,
* url: string,
* position_last_week: ?string,
* mygpo_link: string,
* logo_url: string,
* lastpub: string,
* episode_count: string
* }
*/
interface IProvider interface IProvider
{ {
/** /**
* @return array<array{ * @return Podcast[]
* provider: string,
* id: string,
* title: string,
* author: string,
* image: string,
* provider_url: string,
* feed_url: string,
* last_pub: string,
* nb_episodes: string
* }>
*/ */
public function search(string $value): array; public function search(string $value): array;
} }

View File

@ -34,15 +34,18 @@ class ItunesService implements IProvider
/** @var string[] $feed */ /** @var string[] $feed */
foreach ($json['results'] as $feed) { foreach ($json['results'] as $feed) {
$podcasts[] = [ $podcasts[] = [
'provider' => 'itunes',
'id' => $feed['id'], 'id' => $feed['id'],
'provider' => 'itunes',
'website' => $feed['trackViewUrl'],
'description' => $feed['primaryGenreName'],
'title' => $feed['trackName'], 'title' => $feed['trackName'],
'author' => $feed['artistName'], 'author' => $feed['artistName'],
'image' => $feed['artworkUrl600'], 'url' => $feed['feedUrl'],
'provider_url' => $feed['trackViewUrl'], 'position_last_week' => null,
'feed_url' => $feed['feedUrl'], 'mygpo_link' => $feed['trackViewUrl'],
'last_pub' => $feed['releaseDate'], 'logo_url' => $feed['artworkUrl600'],
'nb_episodes' => $feed['trackCount'], 'lastpub' => $feed['releaseDate'],
'episode_count' => $feed['trackCount'],
]; ];
} }
} }

6302
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,7 +40,7 @@
"@nextcloud/babel-config": "^1.0.0", "@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0", "@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/eslint-config": "^8.2.1", "@nextcloud/eslint-config": "^8.2.1",
"@nextcloud/stylelint-config": "^2.3.0", "@nextcloud/stylelint-config": "^2.3.1",
"@nextcloud/webpack-vue-config": "^5.5.1" "@nextcloud/webpack-vue-config": "^5.5.1"
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<NcAppNavigationItem :loading="loading" <NcAppNavigationItem :loading="loading"
:title="feed ? feed.title : subscriptionUrl" :title="feed ? feed.title : url"
:to="`/${subscriptionUrl}`"> :to="`/${url}`">
<template #icon> <template #icon>
<NcAvatar v-if="feed" <NcAvatar v-if="feed"
:display-name="feed.author" :display-name="feed.author"
@ -29,7 +29,7 @@ import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs' import { showError } from '@nextcloud/dialogs'
export default { export default {
name: 'SubscriptionListItem', name: 'FeedListItem',
components: { components: {
Alert, Alert,
Delete, Delete,
@ -38,7 +38,7 @@ export default {
NcAvatar, NcAvatar,
}, },
props: { props: {
subscriptionUrl: { url: {
type: String, type: String,
required: true, required: true,
}, },
@ -52,7 +52,7 @@ export default {
}, },
async mounted() { async mounted() {
try { try {
const podcastData = await axios.get(generateUrl('/apps/gpoddersync/personal_settings/podcast_data?url={url}', { url: this.subscriptionUrl })) const podcastData = await axios.get(generateUrl('/apps/gpoddersync/personal_settings/podcast_data?url={url}', { url: this.url }))
this.feed = podcastData.data.data this.feed = podcastData.data.data
} catch (e) { } catch (e) {
this.failed = true this.failed = true
@ -65,7 +65,7 @@ export default {
async deleteSubscription() { async deleteSubscription() {
try { try {
this.loading = true this.loading = true
await axios.post(generateUrl('/apps/gpoddersync/subscription_change/create'), { add: [], remove: [this.subscriptionUrl] }) await axios.post(generateUrl('/apps/gpoddersync/subscription_change/create'), { add: [], remove: [this.url] })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
showError(t('Error while removing the feed')) showError(t('Error while removing the feed'))

View File

@ -2,14 +2,14 @@
<ul> <ul>
<NcListItem v-for="feed in feeds" <NcListItem v-for="feed in feeds"
:key="`${feed.provider}_${feed.id}`" :key="`${feed.provider}_${feed.id}`"
:counter-number="feed.nb_episodes" :counter-number="feed.episode_count"
:details="formatTimeAgo(new Date(feed.last_pub))" :details="formatTimeAgo(new Date(feed.lastpub))"
:title="feed.title" :title="feed.title"
:to="`/${feed.feed_url}`"> :to="`/${feed.url}`">
<template #icon> <template #icon>
<NcAvatar :display-name="feed.author" <NcAvatar :display-name="feed.author"
:is-no-user="true" :is-no-user="true"
:url="feed.image" /> :url="feed.logo_url" />
</template> </template>
<template #subtitle> <template #subtitle>
{{ feed.author }} {{ feed.author }}

View File

@ -1,7 +1,7 @@
<template> <template>
<a @click="addSubscription"> <a @click="addSubscription">
<img :alt="author" <img :alt="`${title} - ${author}`"
:src="imgUrl" :src="logo"
:title="author"> :title="author">
</a> </a>
</template> </template>
@ -15,23 +15,27 @@ export default {
name: 'TopItem', name: 'TopItem',
components: {}, components: {},
props: { props: {
xmlUrl: {
type: String,
required: true,
},
imgUrl: {
type: String,
required: true,
},
author: { author: {
type: String, type: String,
required: true, required: true,
}, },
logo: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
}, },
methods: { methods: {
async addSubscription() { async addSubscription() {
try { try {
await axios.post(generateUrl('/apps/gpoddersync/subscription_change/create'), { add: [this.xmlUrl], remove: [] }) await axios.post(generateUrl('/apps/gpoddersync/subscription_change/create'), { add: [this.url], remove: [] })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
showError(t('Error while adding the feed')) showError(t('Error while adding the feed'))

View File

@ -8,8 +8,9 @@
<ul v-if="!loading" class="tops"> <ul v-if="!loading" class="tops">
<li v-for="top in tops" :key="top.id"> <li v-for="top in tops" :key="top.id">
<TopItem :author="top.author" <TopItem :author="top.author"
:img-url="top.imgURL" :logo="top.logo_url"
:xml-url="top.xmlURL" /> :title="top.title"
:url="top.url" />
</li> </li>
</ul> </ul>
<span class="caption">{{ t('Suggests by fyyd') }}</span> <span class="caption">{{ t('Suggests by fyyd') }}</span>
@ -39,8 +40,8 @@ export default {
async mounted() { async mounted() {
try { try {
this.loading = true this.loading = true
const top = await axios.get(generateUrl('/apps/repod/top')) const toplist = await axios.get(generateUrl('/apps/repod/toplist/10'))
this.tops = top.data.data this.tops = toplist.data
} catch (e) { } catch (e) {
console.error(e) console.error(e)
showError(t('Could not fetch tops')) showError(t('Could not fetch tops'))

View File

@ -11,9 +11,9 @@
</router-link> </router-link>
<NcLoadingIcon v-if="loading" /> <NcLoadingIcon v-if="loading" />
<ul v-if="!loading"> <ul v-if="!loading">
<SubscriptionListItem v-for="subscriptionUrl of subscriptions" <FeedListItem v-for="subscriptionUrl of subscriptions"
:key="subscriptionUrl" :key="subscriptionUrl"
:subscription-url="subscriptionUrl" /> :url="subscriptionUrl" />
</ul> </ul>
</NcAppContentList> </NcAppContentList>
</NcAppNavigation> </NcAppNavigation>
@ -31,20 +31,20 @@ import {
NcAppNavigationNew, NcAppNavigationNew,
NcLoadingIcon, NcLoadingIcon,
} from '@nextcloud/vue' } from '@nextcloud/vue'
import FeedListItem from '../components/FeedListItem.vue'
import Plus from 'vue-material-design-icons/Plus.vue' import Plus from 'vue-material-design-icons/Plus.vue'
import SubscriptionListItem from '../components/SubscriptionListItem.vue'
import { showError } from '@nextcloud/dialogs' import { showError } from '@nextcloud/dialogs'
export default { export default {
name: 'Index', name: 'Index',
components: { components: {
FeedListItem,
NcAppContent, NcAppContent,
NcAppContentList, NcAppContentList,
NcAppNavigation, NcAppNavigation,
NcAppNavigationNew, NcAppNavigationNew,
NcLoadingIcon, NcLoadingIcon,
Plus, Plus,
SubscriptionListItem,
}, },
data() { data() {
return { return {