Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added view of synced subs in personal settings #85

Merged
merged 35 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
81d86f2
Initial test version
applejag Jul 10, 2022
37b48d5
Moved personal metrics to API endpoint
applejag Jul 17, 2022
2b34249
Added fetched podcast data
applejag Jul 17, 2022
e5b1d62
Smallfix errors
applejag Jul 17, 2022
7b889eb
Added more info to list view
applejag Jul 17, 2022
950bcc9
Updated GitHub Action to also build NPM
applejag Jul 17, 2022
0f52921
Added CI action for building JS
applejag Jul 17, 2022
0c4fb72
tabs -> spaces
applejag Jul 17, 2022
a9e6cc1
Changed defaultSubscriptionData to static
applejag Jul 17, 2022
0a59758
Removed old code
applejag Jul 17, 2022
ea82f98
Removed 'no settings' note from README.md
applejag Jul 17, 2022
a32a3e2
Ran composer update
applejag Aug 7, 2022
3afa358
Ignore phpunit cache
applejag Aug 7, 2022
2d2f127
Removed unused namespaces
applejag Aug 28, 2022
e406a91
Smallfix for TS intellisense
applejag Aug 28, 2022
8d79f25
Refactored code out to Core ns and data classes
applejag Aug 28, 2022
1a4effc
Renamed fields in Vue page
applejag Sep 17, 2022
c97344b
Extracted fromArray into new function
applejag Sep 17, 2022
3eb23e1
Added PodcastData parsing/toArray tests
applejag Sep 17, 2022
593079b
Changed to implement JsonSerializable
applejag Sep 17, 2022
41a8ad4
Extracted PodcastData to its own endpoint
applejag Sep 17, 2022
c8a090c
Extracted to separate Vue component
applejag Sep 17, 2022
a96c7ce
Updated npm packages
applejag Sep 17, 2022
2787385
Added image proxying/caching
applejag Sep 17, 2022
371094f
Added basic sorting
applejag Sep 17, 2022
0789195
Removed trailing semicolon
applejag Sep 17, 2022
9b0364b
Added JsonSerializable on ActionCounts as well
applejag Sep 17, 2022
89b8052
Cleaned up unused image_proxy endpoint
applejag Sep 17, 2022
21ac60f
Removed unused code & check for userId==null
applejag Sep 18, 2022
23c8550
Extracted properties to methods
applejag Sep 18, 2022
ed43a40
Removed unnecessary userId=null checks
applejag Sep 18, 2022
842a0f6
Apply suggestions from code review
applejag Oct 3, 2022
6d82a29
Apply suggestions from code review
applejag Oct 26, 2022
88821e0
Removed trailing comma in __construct params
applejag Oct 26, 2022
a1b0894
Apply suggestions from code review
applejag Oct 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
extends: [
'@nextcloud',
]
}
6 changes: 5 additions & 1 deletion .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ jobs:
uses: actions/checkout@v2
with:
path: ${{ env.APP_NAME }}
- name: Run build
- name: Install NPM packages
run: cd ${{ env.APP_NAME }} && make npm-init
- name: Build JS
run: cd ${{ env.APP_NAME }} && make build-js-production
- name: Create release tarball
run: cd ${{ env.APP_NAME }} && make appstore
- name: Upload app tarball to release
uses: svenstaro/upload-release-action@v2
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/ci-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: NPM build

on:
pull_request:

env:
APP_NAME: gpoddersync

jobs:
js:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2
with:
path: ${{ env.APP_NAME }}
- name: Install NPM packages
run: cd ${{ env.APP_NAME }} && make npm-init
- name: Build JS
run: cd ${{ env.APP_NAME }} && make build-js-production
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
vendor/*
tests/.phpunit.result.cache
node_modules/
js/
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ Nextcloud app that replicates basic gpodder.net api to sync podcast consumer app
### Installation
Either from the official Nextcloud app store ([link to app page](https://apps.nextcloud.com/apps/gpoddersync)) or by downloading the [latest release](https://github.com/thrillfall/nextcloud-gpodder/releases/latest) and extracting it into your Nextcloud apps/ directory.

馃挕 There is no app icon since there are no settings to be set.

## API
### subscription
* **get subscription changes**: `GET /index.php/apps/gpoddersync/subscriptions`
Expand Down
4 changes: 4 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@
<step>OCA\GPodderSync\Migration\TimestampMigration</step>
</post-migration>
</repair-steps>
<settings>
<personal>OCA\GPodderSync\Settings\GPodderSyncPersonal</personal>
<personal-section>OCA\GPodderSync\Sections\GPodderSyncPersonal</personal-section>
</settings>
</info>
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@

['name' => 'subscription_change#list', 'url' => '/subscriptions', 'verb' => 'GET'],
['name' => 'subscription_change#create', 'url' => '/subscription_change/create', 'verb' => 'POST'],
['name' => 'personal_settings#metrics', 'url' => '/personal_settings/metrics', 'verb' => 'GET'],
['name' => 'personal_settings#podcastData', 'url' => '/personal_settings/podcast_data', 'verb' => 'GET'],
]
];
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const babelConfig = require('@nextcloud/babel-config')

module.exports = babelConfig
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
{
"name": "thrillfall"
}
]
],
"require-dev": {
"phpunit/phpunit": "^9"
}
}
13 changes: 13 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"jsx": "preserve"
},
"exclude": [
"node_modules"
],
"include": [
"src/**/*"
]
}
65 changes: 65 additions & 0 deletions lib/Controller/PersonalSettingsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);

namespace OCA\GPodderSync\Controller;

use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader;

use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;

class PersonalSettingsController extends Controller {

private string $userId;
private PodcastMetricsReader $metricsReader;
private PodcastDataReader $dataReader;

public function __construct(
string $AppName,
IRequest $request,
?string $UserId,
PodcastMetricsReader $metricsReader,
PodcastDataReader $dataReader
) {
parent::__construct($AppName, $request);
$this->userId = $UserId ?? '';
$this->metricsReader = $metricsReader;
$this->dataReader = $dataReader;
}

/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @return JSONResponse
*/
public function metrics(): JSONResponse {
applejag marked this conversation as resolved.
Show resolved Hide resolved
$metrics = $this->metricsReader->metrics($this->userId);
return new JSONResponse([
'subscriptions' => $metrics,
]);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $url
* @return JsonResponse
*/
public function podcastData(string $url = ''): JsonResponse {
if ($url === '') {
return new JSONResponse([
'message' => "Missing query parameter 'url'.",
'data' => null,
], statusCode: Http::STATUS_BAD_REQUEST);
applejag marked this conversation as resolved.
Show resolved Hide resolved
}
return new JsonResponse([
'data' => $this->dataReader->getCachedOrFetchPodcastData($url, $this->userId),
]);
}
}
47 changes: 47 additions & 0 deletions lib/Core/PodcastData/PodcastActionCounts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);

namespace OCA\GPodderSync\Core\PodcastData;

use JsonSerializable;

class PodcastActionCounts implements JsonSerializable {
private int $delete = 0;
private int $download = 0;
private int $flattr = 0;
private int $new = 0;
private int $play = 0;

/**
* @param string $action
*/
public function incrementAction(string $action): void {
switch ($action) {
case 'delete': $this->delete++; break;
case 'download': $this->download++; break;
case 'flattr': $this->flattr++; break;
case 'new': $this->new++; break;
case 'play': $this->play++; break;
}
}

/**
* @return array<string,int>
*/
public function toArray(): array {
return [
'delete' => $this->delete,
'download' => $this->download,
'flattr' => $this->flattr,
'new' => $this->new,
'play' => $this->play,
];
}

/**
* @return array<string,int>
*/
public function jsonSerialize(): mixed {
return $this->toArray();
}
}
181 changes: 181 additions & 0 deletions lib/Core/PodcastData/PodcastData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);

namespace OCA\GPodderSync\Core\PodcastData;

use DateTime;
use JsonSerializable;
use SimpleXMLElement;

class PodcastData implements JsonSerializable {
private ?string $title;
private ?string $author;
private ?string $link;
private ?string $description;
private ?string $imageUrl;
private int $fetchedAtUnix;
private ?string $imageBlob;

public function __construct(
?string $title,
?string $author,
?string $link,
?string $description,
?string $imageUrl,
int $fetchedAtUnix,
?string $imageBlob = null
) {
$this->title = $title;
$this->author = $author;
$this->link = $link;
$this->description = $description;
$this->imageUrl = $imageUrl;
$this->fetchedAtUnix = $fetchedAtUnix;
$this->imageBlob = $imageBlob;
}

/**
* @return PodcastData
* @throws Exception if the XML data could not be parsed.
*/
public static function parseRssXml(string $xmlString, ?int $fetchedAtUnix = null): PodcastData {
$xml = new SimpleXMLElement($xmlString);
$channel = $xml->channel;
return new PodcastData(
title: self::stringOrNull($channel->title),
author: self::getXPathContent($xml, '/rss/channel/itunes:author'),
link: self::stringOrNull($channel->link),
description: self::stringOrNull($channel->description),
imageUrl:
self::getXPathContent($xml, '/rss/channel/image/url')
?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href'),
fetchedAtUnix: $fetchedAtUnix ?? (new DateTime())->getTimestamp(),
applejag marked this conversation as resolved.
Show resolved Hide resolved
);
}

private static function stringOrNull(mixed $value): ?string {
applejag marked this conversation as resolved.
Show resolved Hide resolved
if ($value) {
return (string)$value;
}
return null;
}

private static function getXPathContent(SimpleXMLElement $xml, string $xpath): ?string {
$match = $xml->xpath($xpath);
if ($match) {
return (string)$match[0];
}
return null;
}

private static function getXPathAttribute(SimpleXMLElement $xml, string $xpath): ?string {
$match = $xml->xpath($xpath);
if ($match) {
return (string)$match[0][0];
}
return null;
}

/**
* @return string|null
*/
public function getTitle(): ?string {
return $this->title;
}

/**
* @return string|null
*/
public function getAuthor(): ?string {
return $this->author;
}

/**
* @return string|null
*/
public function getLink(): ?string {
return $this->link;
}

/**
* @return string|null
*/
public function getDescription(): ?string {
return $this->description;
}

/**
* @return string|null
*/
public function getImageUrl(): ?string {
return $this->imageUrl;
}

/**
* @return int|null
*/
public function getFetchedAtUnix(): ?int {
return $this->fetchedAtUnix;
}

/**
* @return string|null
*/
public function getImageBlob(): ?string {
return $this->imageBlob;
}

/**
* @param string $blob
* @return void
*/
public function setImageBlob(?string $blob): void {
$this->imageBlob = $blob;
}

/**
* @return string
*/
public function __toString() : string {
return $this->title ?? '/no title/';
}

/**
* @return array<string,mixed>
*/
public function toArray(): array {
return
[
'title' => $this->title,
'author' => $this->author,
'link' => $this->link,
'description' => $this->description,
'imageUrl' => $this->imageUrl,
'imageBlob' => $this->imageBlob,
'fetchedAtUnix' => $this->fetchedAtUnix,
];
}

/**
* @return array<string,mixed>
*/
public function jsonSerialize(): mixed {
applejag marked this conversation as resolved.
Show resolved Hide resolved
return $this->toArray();
}

/**
* @return PodcastData
*/
public static function fromArray(array $data): PodcastData {
return new PodcastData(
title: $data['title'],
author: $data['author'],
link: $data['link'],
description: $data['description'],
imageUrl: $data['imageUrl'],
fetchedAtUnix: $data['fetchedAtUnix'],
imageBlob: $data['imageBlob'],
applejag marked this conversation as resolved.
Show resolved Hide resolved
);
}
}