Added image proxying/caching

This commit is contained in:
Kalle Fagerberg 2022-09-17 21:03:14 +02:00 committed by thrillfall
parent 5d59cfde56
commit a5a69fa3f7
6 changed files with 111 additions and 43 deletions

View File

@ -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'],
]
];

View File

@ -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();
}
/**

View File

@ -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'],
);
}
}

View File

@ -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 {

View File

@ -3,7 +3,7 @@
:details="formatSubscriptionDetails(sub)">
<template #icon>
<Avatar :size="44"
:url="podcastData?.image"
:url="podcastData?.imageBlob ?? podcastData?.imageUrl"
:display-name="podcastData?.author" />
</template>
<template #subtitle>

View File

@ -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());