diff --git a/appinfo/routes.php b/appinfo/routes.php
index 1e49c26..a33e08d 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -16,5 +16,6 @@ return [
['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'],
+ ['name' => 'personal_settings#imageProxy', 'url' => '/personal_settings/image_proxy', 'verb' => 'GET'],
]
];
diff --git a/lib/Controller/PersonalSettingsController.php b/lib/Controller/PersonalSettingsController.php
index ae676dc..2031405 100644
--- a/lib/Controller/PersonalSettingsController.php
+++ b/lib/Controller/PersonalSettingsController.php
@@ -3,12 +3,18 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Controller;
+use GuzzleHttp\Psr7\BufferStream;
+use GuzzleHttp\Psr7\StreamWrapper;
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\AppFramework\Http\StreamResponse;
+use OCP\AppFramework\OCS\OCSException;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
use OCP\IRequest;
class PersonalSettingsController extends Controller {
@@ -17,17 +23,22 @@ class PersonalSettingsController extends Controller {
private PodcastMetricsReader $metricsReader;
private PodcastDataReader $dataReader;
+ // TODO: Use httpClient via PodcastDataReader instead
+ private IClient $httpClient;
+
public function __construct(
string $AppName,
IRequest $request,
string $UserId,
PodcastMetricsReader $metricsReader,
PodcastDataReader $dataReader,
+ IClientService $httpClientService,
) {
parent::__construct($AppName, $request);
$this->userId = $UserId ?? '';
$this->metricsReader = $metricsReader;
$this->dataReader = $dataReader;
+ $this->httpClient = $httpClientService->newClient();
}
/**
diff --git a/lib/Core/PodcastData/PodcastData.php b/lib/Core/PodcastData/PodcastData.php
index e23b40f..648379e 100644
--- a/lib/Core/PodcastData/PodcastData.php
+++ b/lib/Core/PodcastData/PodcastData.php
@@ -8,27 +8,30 @@ use JsonSerializable;
use SimpleXMLElement;
class PodcastData implements JsonSerializable {
- private string $title;
- private string $author;
- private string $link;
- private string $description;
- private string $image;
+ 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 $image,
+ ?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->image = $image;
+ $this->imageUrl = $imageUrl;
$this->fetchedAtUnix = $fetchedAtUnix;
+ $this->imageBlob = $imageBlob;
}
/**
@@ -39,17 +42,24 @@ class PodcastData implements JsonSerializable {
$xml = new SimpleXMLElement($xmlString);
$channel = $xml->channel;
return new PodcastData(
- title: (string)$channel->title,
- author: (string)self::getXPathContent($xml, '/rss/channel/itunes:author'),
- link: (string)$channel->link,
- description: (string)$channel->description,
- image:
- (string)(self::getXPathContent($xml, '/rss/channel/image/url')
- ?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href')),
+ 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(),
);
}
+ private static function stringOrNull(mixed $value): ?string {
+ if ($value) {
+ return (string)$value;
+ }
+ return null;
+ }
+
private static function getXPathContent(SimpleXMLElement $xml, string $xpath): ?string {
$match = $xml->xpath($xpath);
if ($match) {
@@ -67,52 +77,67 @@ class PodcastData implements JsonSerializable {
}
/**
- * @return string
+ * @return string|null
*/
- public function getTitle(): string {
+ public function getTitle(): ?string {
return $this->title;
}
/**
- * @return string
+ * @return string|null
*/
- public function getAuthor(): string {
+ public function getAuthor(): ?string {
return $this->author;
}
/**
- * @return string
+ * @return string|null
*/
- public function getLink(): string {
+ public function getLink(): ?string {
return $this->link;
}
/**
- * @return string
+ * @return string|null
*/
- public function getDescription(): string {
+ public function getDescription(): ?string {
return $this->description;
}
/**
- * @return string
+ * @return string|null
*/
- public function getImage(): string {
- return $this->image;
+ public function getImageUrl(): ?string {
+ return $this->imageUrl;
}
/**
- * @return int
+ * @return int|null
*/
- public function getFetchedAtUnix(): int {
+ 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;
+ public function __toString() : string {
+ return $this->title ?? '/no title/';
}
/**
@@ -125,7 +150,8 @@ class PodcastData implements JsonSerializable {
'author' => $this->author,
'link' => $this->link,
'description' => $this->description,
- 'image' => $this->image,
+ 'imageUrl' => $this->imageUrl,
+ 'imageBlob' => $this->imageBlob,
'fetchedAtUnix' => $this->fetchedAtUnix,
];
}
@@ -146,8 +172,9 @@ class PodcastData implements JsonSerializable {
author: $data['author'],
link: $data['link'],
description: $data['description'],
- image: $data['image'],
+ imageUrl: $data['imageUrl'],
fetchedAtUnix: $data['fetchedAtUnix'],
+ imageBlob: $data['imageBlob'],
);
}
}
diff --git a/lib/Core/PodcastData/PodcastDataReader.php b/lib/Core/PodcastData/PodcastDataReader.php
index 0dbdbb0..80a6049 100644
--- a/lib/Core/PodcastData/PodcastDataReader.php
+++ b/lib/Core/PodcastData/PodcastDataReader.php
@@ -3,9 +3,11 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
+use Exception;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -48,13 +50,37 @@ class PodcastDataReader {
if (!$this->userHasPodcast($url, $userId)) {
return null;
}
+ $resp = $this->fetchUrl($url);
+ $data = PodcastData::parseRssXml($resp->getBody());
+ $blob = $this->tryFetchImageBlob($data);
+ if ($blob) {
+ $data->setImageBlob($blob);
+ }
+ return $data;
+ }
+
+ private function tryFetchImageBlob(PodcastData $data): ?string {
+ if (!$data->getImageUrl()) {
+ return null;
+ }
+ try {
+ $resp = $this->fetchUrl($data->getImageUrl());
+ $contentType = $resp->getHeader('Content-Type');
+ $body = $resp->getBody();
+ $bodyBase64 = base64_encode($body);
+ return "data:$contentType;base64,$bodyBase64";
+ } catch (Exception) {
+ return null;
+ }
+ }
+
+ private function fetchUrl(string $url): IResponse {
$resp = $this->httpClient->get($url);
$statusCode = $resp->getStatusCode();
if ($statusCode < 200 || $statusCode >= 300) {
- throw new \ErrorException("Podcast RSS URL returned non-2xx status code: $statusCode");
+ throw new \ErrorException("Web request returned non-2xx status code: $statusCode");
}
- $body = $resp->getBody();
- return PodcastData::parseRssXml($body);
+ return $resp;
}
public function tryGetCachedPodcastData(string $url): ?PodcastData {
diff --git a/src/components/SubscriptionListItem.vue b/src/components/SubscriptionListItem.vue
index 5a15cb2..797044c 100644
--- a/src/components/SubscriptionListItem.vue
+++ b/src/components/SubscriptionListItem.vue
@@ -3,7 +3,7 @@
:details="formatSubscriptionDetails(sub)">
diff --git a/tests/Unit/Core/PodcastData/PodcastDataTest.php b/tests/Unit/Core/PodcastData/PodcastDataTest.php
index b7b2e18..4296fd8 100644
--- a/tests/Unit/Core/PodcastData/PodcastDataTest.php
+++ b/tests/Unit/Core/PodcastData/PodcastDataTest.php
@@ -14,7 +14,8 @@ class EpisodeActionTest extends TestCase {
'author' => 'author1',
'link' => 'http://example.com/',
'description' => 'description1',
- 'image' => 'http://example.com/image.jpg',
+ 'imageUrl' => 'http://example.com/image.jpg',
+ 'imageBlob' => null,
'fetchedAtUnix' => 1337,
];
$this->assertSame($expected, $podcastData->toArray());
@@ -69,7 +70,8 @@ class EpisodeActionTest extends TestCase {
'author' => 'The Podcast Author',
'link' => 'http://example.com',
'description' => 'Some long description',
- 'image' => 'https://example.com/image.jpg',
+ 'imageUrl' => 'https://example.com/image.jpg',
+ 'imageBlob' => null,
'fetchedAtUnix' => 1337,
];
$this->assertSame($expected, $podcastData->toArray());
@@ -102,8 +104,9 @@ class EpisodeActionTest extends TestCase {
'title' => 'The title of this Podcast',
'author' => 'The Podcast Author',
'link' => 'http://example.com',
- 'description' => '',
- 'image' => '',
+ 'description' => null,
+ 'imageUrl' => null,
+ 'imageBlob' => null,
'fetchedAtUnix' => 1337,
];
$this->assertSame($expected, $podcastData->toArray());