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' => 'subscription_change#create', 'url' => '/subscription_change/create', 'verb' => 'POST'],
|
||||||
['name' => 'personal_settings#metrics', 'url' => '/personal_settings/metrics', 'verb' => 'GET'],
|
['name' => 'personal_settings#metrics', 'url' => '/personal_settings/metrics', 'verb' => 'GET'],
|
||||||
['name' => 'personal_settings#podcastData', 'url' => '/personal_settings/podcast_data', '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;
|
namespace OCA\GPodderSync\Controller;
|
||||||
|
|
||||||
|
use GuzzleHttp\Psr7\BufferStream;
|
||||||
|
use GuzzleHttp\Psr7\StreamWrapper;
|
||||||
use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
|
use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
|
||||||
use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader;
|
use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader;
|
||||||
|
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
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;
|
use OCP\IRequest;
|
||||||
|
|
||||||
class PersonalSettingsController extends Controller {
|
class PersonalSettingsController extends Controller {
|
||||||
@ -17,17 +23,22 @@ class PersonalSettingsController extends Controller {
|
|||||||
private PodcastMetricsReader $metricsReader;
|
private PodcastMetricsReader $metricsReader;
|
||||||
private PodcastDataReader $dataReader;
|
private PodcastDataReader $dataReader;
|
||||||
|
|
||||||
|
// TODO: Use httpClient via PodcastDataReader instead
|
||||||
|
private IClient $httpClient;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $AppName,
|
string $AppName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
string $UserId,
|
string $UserId,
|
||||||
PodcastMetricsReader $metricsReader,
|
PodcastMetricsReader $metricsReader,
|
||||||
PodcastDataReader $dataReader,
|
PodcastDataReader $dataReader,
|
||||||
|
IClientService $httpClientService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($AppName, $request);
|
parent::__construct($AppName, $request);
|
||||||
$this->userId = $UserId ?? '';
|
$this->userId = $UserId ?? '';
|
||||||
$this->metricsReader = $metricsReader;
|
$this->metricsReader = $metricsReader;
|
||||||
$this->dataReader = $dataReader;
|
$this->dataReader = $dataReader;
|
||||||
|
$this->httpClient = $httpClientService->newClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,27 +8,30 @@ use JsonSerializable;
|
|||||||
use SimpleXMLElement;
|
use SimpleXMLElement;
|
||||||
|
|
||||||
class PodcastData implements JsonSerializable {
|
class PodcastData implements JsonSerializable {
|
||||||
private string $title;
|
private ?string $title;
|
||||||
private string $author;
|
private ?string $author;
|
||||||
private string $link;
|
private ?string $link;
|
||||||
private string $description;
|
private ?string $description;
|
||||||
private string $image;
|
private ?string $imageUrl;
|
||||||
private int $fetchedAtUnix;
|
private int $fetchedAtUnix;
|
||||||
|
private ?string $imageBlob;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $title,
|
?string $title,
|
||||||
string $author,
|
?string $author,
|
||||||
string $link,
|
?string $link,
|
||||||
string $description,
|
?string $description,
|
||||||
string $image,
|
?string $imageUrl,
|
||||||
int $fetchedAtUnix,
|
int $fetchedAtUnix,
|
||||||
|
?string $imageBlob = null,
|
||||||
) {
|
) {
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
$this->author = $author;
|
$this->author = $author;
|
||||||
$this->link = $link;
|
$this->link = $link;
|
||||||
$this->description = $description;
|
$this->description = $description;
|
||||||
$this->image = $image;
|
$this->imageUrl = $imageUrl;
|
||||||
$this->fetchedAtUnix = $fetchedAtUnix;
|
$this->fetchedAtUnix = $fetchedAtUnix;
|
||||||
|
$this->imageBlob = $imageBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,17 +42,24 @@ class PodcastData implements JsonSerializable {
|
|||||||
$xml = new SimpleXMLElement($xmlString);
|
$xml = new SimpleXMLElement($xmlString);
|
||||||
$channel = $xml->channel;
|
$channel = $xml->channel;
|
||||||
return new PodcastData(
|
return new PodcastData(
|
||||||
title: (string)$channel->title,
|
title: self::stringOrNull($channel->title),
|
||||||
author: (string)self::getXPathContent($xml, '/rss/channel/itunes:author'),
|
author: self::getXPathContent($xml, '/rss/channel/itunes:author'),
|
||||||
link: (string)$channel->link,
|
link: self::stringOrNull($channel->link),
|
||||||
description: (string)$channel->description,
|
description: self::stringOrNull($channel->description),
|
||||||
image:
|
imageUrl:
|
||||||
(string)(self::getXPathContent($xml, '/rss/channel/image/url')
|
self::getXPathContent($xml, '/rss/channel/image/url')
|
||||||
?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href')),
|
?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href'),
|
||||||
fetchedAtUnix: $fetchedAtUnix ?? (new DateTime())->getTimestamp(),
|
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 {
|
private static function getXPathContent(SimpleXMLElement $xml, string $xpath): ?string {
|
||||||
$match = $xml->xpath($xpath);
|
$match = $xml->xpath($xpath);
|
||||||
if ($match) {
|
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 $this->title;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function getAuthor(): string {
|
public function getAuthor(): ?string {
|
||||||
return $this->author;
|
return $this->author;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function getLink(): string {
|
public function getLink(): ?string {
|
||||||
return $this->link;
|
return $this->link;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function getDescription(): string {
|
public function getDescription(): ?string {
|
||||||
return $this->description;
|
return $this->description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function getImage(): string {
|
public function getImageUrl(): ?string {
|
||||||
return $this->image;
|
return $this->imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
public function getFetchedAtUnix(): int {
|
public function getFetchedAtUnix(): ?int {
|
||||||
return $this->fetchedAtUnix;
|
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
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function __toString() : String {
|
public function __toString() : string {
|
||||||
return $this->title;
|
return $this->title ?? '/no title/';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,7 +150,8 @@ class PodcastData implements JsonSerializable {
|
|||||||
'author' => $this->author,
|
'author' => $this->author,
|
||||||
'link' => $this->link,
|
'link' => $this->link,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'image' => $this->image,
|
'imageUrl' => $this->imageUrl,
|
||||||
|
'imageBlob' => $this->imageBlob,
|
||||||
'fetchedAtUnix' => $this->fetchedAtUnix,
|
'fetchedAtUnix' => $this->fetchedAtUnix,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -146,8 +172,9 @@ class PodcastData implements JsonSerializable {
|
|||||||
author: $data['author'],
|
author: $data['author'],
|
||||||
link: $data['link'],
|
link: $data['link'],
|
||||||
description: $data['description'],
|
description: $data['description'],
|
||||||
image: $data['image'],
|
imageUrl: $data['imageUrl'],
|
||||||
fetchedAtUnix: $data['fetchedAtUnix'],
|
fetchedAtUnix: $data['fetchedAtUnix'],
|
||||||
|
imageBlob: $data['imageBlob'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace OCA\GPodderSync\Core\PodcastData;
|
namespace OCA\GPodderSync\Core\PodcastData;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
|
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
|
||||||
use OCP\Http\Client\IClient;
|
use OCP\Http\Client\IClient;
|
||||||
use OCP\Http\Client\IClientService;
|
use OCP\Http\Client\IClientService;
|
||||||
|
use OCP\Http\Client\IResponse;
|
||||||
use OCP\ICache;
|
use OCP\ICache;
|
||||||
use OCP\ICacheFactory;
|
use OCP\ICacheFactory;
|
||||||
|
|
||||||
@ -48,13 +50,37 @@ class PodcastDataReader {
|
|||||||
if (!$this->userHasPodcast($url, $userId)) {
|
if (!$this->userHasPodcast($url, $userId)) {
|
||||||
return null;
|
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);
|
$resp = $this->httpClient->get($url);
|
||||||
$statusCode = $resp->getStatusCode();
|
$statusCode = $resp->getStatusCode();
|
||||||
if ($statusCode < 200 || $statusCode >= 300) {
|
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 $resp;
|
||||||
return PodcastData::parseRssXml($body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tryGetCachedPodcastData(string $url): ?PodcastData {
|
public function tryGetCachedPodcastData(string $url): ?PodcastData {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
:details="formatSubscriptionDetails(sub)">
|
:details="formatSubscriptionDetails(sub)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Avatar :size="44"
|
<Avatar :size="44"
|
||||||
:url="podcastData?.image"
|
:url="podcastData?.imageBlob ?? podcastData?.imageUrl"
|
||||||
:display-name="podcastData?.author" />
|
:display-name="podcastData?.author" />
|
||||||
</template>
|
</template>
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
|
@ -14,7 +14,8 @@ class EpisodeActionTest extends TestCase {
|
|||||||
'author' => 'author1',
|
'author' => 'author1',
|
||||||
'link' => 'http://example.com/',
|
'link' => 'http://example.com/',
|
||||||
'description' => 'description1',
|
'description' => 'description1',
|
||||||
'image' => 'http://example.com/image.jpg',
|
'imageUrl' => 'http://example.com/image.jpg',
|
||||||
|
'imageBlob' => null,
|
||||||
'fetchedAtUnix' => 1337,
|
'fetchedAtUnix' => 1337,
|
||||||
];
|
];
|
||||||
$this->assertSame($expected, $podcastData->toArray());
|
$this->assertSame($expected, $podcastData->toArray());
|
||||||
@ -69,7 +70,8 @@ class EpisodeActionTest extends TestCase {
|
|||||||
'author' => 'The Podcast Author',
|
'author' => 'The Podcast Author',
|
||||||
'link' => 'http://example.com',
|
'link' => 'http://example.com',
|
||||||
'description' => 'Some long description',
|
'description' => 'Some long description',
|
||||||
'image' => 'https://example.com/image.jpg',
|
'imageUrl' => 'https://example.com/image.jpg',
|
||||||
|
'imageBlob' => null,
|
||||||
'fetchedAtUnix' => 1337,
|
'fetchedAtUnix' => 1337,
|
||||||
];
|
];
|
||||||
$this->assertSame($expected, $podcastData->toArray());
|
$this->assertSame($expected, $podcastData->toArray());
|
||||||
@ -102,8 +104,9 @@ class EpisodeActionTest extends TestCase {
|
|||||||
'title' => 'The title of this Podcast',
|
'title' => 'The title of this Podcast',
|
||||||
'author' => 'The Podcast Author',
|
'author' => 'The Podcast Author',
|
||||||
'link' => 'http://example.com',
|
'link' => 'http://example.com',
|
||||||
'description' => '',
|
'description' => null,
|
||||||
'image' => '',
|
'imageUrl' => null,
|
||||||
|
'imageBlob' => null,
|
||||||
'fetchedAtUnix' => 1337,
|
'fetchedAtUnix' => 1337,
|
||||||
];
|
];
|
||||||
$this->assertSame($expected, $podcastData->toArray());
|
$this->assertSame($expected, $podcastData->toArray());
|
||||||
|
Loading…
Reference in New Issue
Block a user