Added image proxying/caching
This commit is contained in:
parent
5d59cfde56
commit
a5a69fa3f7
@ -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'],
|
||||
]
|
||||
];
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user