fix(s3): add proxy to serve images from s3 to client

refs #321
This commit is contained in:
Yassine Doghri 2023-04-13 11:45:03 +00:00
parent c5eb6ed590
commit a76724a8cf
74 changed files with 353 additions and 224 deletions

6
.gitignore vendored
View File

@ -143,16 +143,20 @@ node_modules
# public folder
public/*
public/media/site
!public/media
!public/.htaccess
!public/favicon.ico
!public/icon*
!public/castopod-banner*
!public/castopod-avatar*
!public/index.php
!public/robots.txt
!public/.well-known
!public/.well-known/GDPR.yml
public/assets/*
!public/assets/index.html
# public media folder
!public/media/podcasts
!public/media/persons

View File

@ -23,7 +23,7 @@ class Fediverse extends FediverseBaseConfig
*/
public string $noteObject = NoteObject::class;
public string $defaultAvatarImagePath = 'media/castopod-avatar_thumbnail.webp';
public string $defaultAvatarImagePath = 'castopod-avatar_thumbnail.webp';
public string $defaultAvatarImageMimetype = 'image/webp';
@ -52,7 +52,7 @@ class Fediverse extends FediverseBaseConfig
helper('media');
$this->defaultCoverImagePath = media_path($defaultBannerPath . '_federation.' . $extension);
$this->defaultCoverImagePath = $defaultBannerPath . '_federation.' . $extension;
$this->defaultCoverImageMimetype = $defaultBanner['mimetype'];
}
}

View File

@ -52,7 +52,7 @@ class CreditsController extends BaseController
$personId => [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->avatar->thumbnail_url,
get_avatar_url($credit->person, 'thumbnail'),
'information_url' =>
$credit->person->information_url,
'roles' => [
@ -90,7 +90,7 @@ class CreditsController extends BaseController
$credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->avatar->thumbnail_url,
get_avatar_url($credit->person, 'thumbnail'),
'information_url' => $credit->person->information_url,
'roles' => [
$personRole => [

View File

@ -23,7 +23,7 @@ class FeedController extends Controller
{
public function index(string $podcastHandle): ResponseInterface
{
helper(['rss', 'premium_podcasts']);
helper(['rss', 'premium_podcasts', 'misc']);
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
->first();

View File

@ -61,14 +61,12 @@ class WebmanifestController extends Controller
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => service('settings')
->get('App.siteIcon')['192'],
'src' => get_site_icon_url('192'),
'type' => 'image/png',
'sizes' => '192x192',
],
[
'src' => service('settings')
->get('App.siteIcon')['512'],
'src' => get_site_icon_url('512'),
'type' => 'image/png',
'sizes' => '512x512',
],

View File

@ -84,21 +84,10 @@ class Person extends Entity
return $this;
}
public function getAvatar(): Image
public function getAvatar(): ?Image
{
if ($this->attributes['avatar_id'] === null) {
helper('media');
return new Image([
'file_key' => config('Images')
->avatarDefaultPath,
'file_mimetype' => config('Images')
->avatarDefaultMimeType,
'file_size' => 0,
'file_metadata' => [
'sizes' => config('Images')
->personAvatarSizes,
],
], 'fs');
if ($this->avatar_id === null) {
return null;
}
if ($this->avatar === null) {

View File

@ -294,22 +294,10 @@ class Podcast extends Entity
return $this;
}
public function getBanner(): Image
public function getBanner(): ?Image
{
if ($this->banner_id === null) {
$defaultBanner = config('Images')
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
'Images'
)->podcastBannerDefaultPaths['default'];
return new Image([
'file_key' => $defaultBanner['path'],
'file_mimetype' => $defaultBanner['mimetype'],
'file_size' => 0,
'file_metadata' => [
'sizes' => config('Images')
->podcastBannerSizes,
],
], 'fs');
return null;
}
if (! $this->banner instanceof Image) {

View File

@ -33,7 +33,7 @@ if (! function_exists('write_audio_file_tags')) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$APICdata = $fileManager->getFileContents($episode->cover->id3_key);
$APICdata = (string) $fileManager->getFileContents($episode->cover->id3_key);
// TODO: variables used for podcast specific tags
// $podcastUrl = $episode->podcast->link;

View File

@ -2,6 +2,9 @@
declare(strict_types=1);
use App\Entities\Person;
use App\Entities\Podcast;
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -297,3 +300,89 @@ if (! function_exists('format_bytes')) {
return round($bytes, $precision) . $units[$pow];
}
}
if (! function_exists('get_site_icon_url')) {
function get_site_icon_url(string $size): string
{
if (config('App')->siteIcon['ico'] === service('settings')->get('App.siteIcon')['ico']) {
// return default site icon url
return base_url(service('settings')->get('App.siteIcon')[$size]);
}
return service('file_manager')->getUrl(service('settings')->get('App.siteIcon')[$size]);
}
}
if (! function_exists('get_podcast_banner')) {
function get_podcast_banner_url(Podcast $podcast, string $size): string
{
if ($podcast->banner === null) {
$defaultBanner = config('Images')
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
'Images'
)->podcastBannerDefaultPaths['default'];
$sizes = config('Images')
->podcastBannerSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default site icon url
return base_url(
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null)
);
}
$sizeKey = $size . '_url';
return $podcast->banner->{$sizeKey};
}
}
if (! function_exists('get_podcast_banner_mimetype')) {
function get_podcast_banner_mimetype(Podcast $podcast, string $size): string
{
if ($podcast->banner === null) {
$sizes = config('Images')
->podcastBannerSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default site icon url
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
'Images'
)->podcastBannerDefaultMimeType;
}
$mimetype = $size . '_mimetype';
return $podcast->banner->{$mimetype};
}
}
if (! function_exists('get_avatar_url')) {
function get_avatar_url(Person $person, string $size): string
{
if ($person->avatar === null) {
$defaultAvatar = config('Images')
->avatarDefaultPath;
$sizes = config('Images')
->personAvatarSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default site icon url
return base_url(
change_file_path($defaultAvatar['path'], '_' . $size, $sizeConfig['extension'] ?? null)
);
}
$sizeKey = $size . '_url';
return $person->avatar->{$sizeKey};
}
}

View File

@ -204,7 +204,7 @@ if (! function_exists('get_rss_feed')) {
foreach ($person->roles as $role) {
$personElement = $channel->addChild('person', $person->full_name, $podcastNamespace);
$personElement->addAttribute('img', $person->avatar->medium_url);
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
@ -388,7 +388,7 @@ if (! function_exists('get_rss_feed')) {
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
);
$personElement->addAttribute('img', $person->avatar->medium_url);
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);

View File

@ -273,7 +273,7 @@ if (! function_exists('get_home_metatags')) {
$metatags
->title(service('settings')->get('App.siteName'))
->description(esc(service('settings')->get('App.siteDescription')))
->image(service('settings')->get('App.siteIcon')['512'])
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', esc(service('settings')->get('App.siteName')));
@ -292,7 +292,7 @@ if (! function_exists('get_page_metatags')) {
)->get('App.siteName')
)
->description(esc(service('settings')->get('App.siteDescription')))
->image(service('settings')->get('App.siteIcon')['512'])
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', esc(service('settings')->get('App.siteName')));

View File

@ -134,7 +134,7 @@ class VideoClipper
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$jsonTranscriptString = $fileManager->getFileContents($jsonFileKey);
$jsonTranscriptString = (string) $fileManager->getFileContents($jsonFileKey);
if ($jsonTranscriptString === '') {
throw new Exception('Cannot get transcript json contents.');
}

View File

@ -469,8 +469,8 @@ class PodcastModel extends Model
$actor->summary = $podcast->description_html;
$actor->avatar_image_url = $podcast->cover->federation_url;
$actor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
$actor->cover_image_url = $podcast->banner->federation_url;
$actor->cover_image_mimetype = $podcast->banner->federation_mimetype;
$actor->cover_image_url = get_podcast_banner_url($podcast, 'federation');
$actor->cover_image_mimetype = get_podcast_banner_mimetype($podcast, 'federation');
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);

View File

@ -17,10 +17,10 @@ use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\Entities\Audio;
use Modules\Media\FileManagers\FileManagerInterface;
use Modules\Media\FileManagers\FS;
use Modules\Media\Models\MediaModel;
use PHP_ICO;
@ -58,47 +58,60 @@ class SettingsController extends BaseController
$siteIconFile = $this->request->getFile('site_icon');
if ($siteIconFile !== null && $siteIconFile->isValid()) {
helper(['filesystem', 'media']);
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
// delete site folder in media before repopulating it
delete_files(media_path_absolute('/site'));
// save original in disk
$originalFilename = (new FS(config('Media')))->save(
$siteIconFile,
'site/icon.' . $siteIconFile->getExtension()
);
$fileManager->deleteAll('site');
// convert jpeg image to png if not
if ($siteIconFile->getClientMimeType() !== 'image/png') {
service('image')->withFile(media_path_absolute($originalFilename))
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
service('image')
->withFile($siteIconFile->getRealPath())
->convert(IMAGETYPE_JPEG)
->save(media_path_absolute('/site/icon.png'));
->save($tempFilePath);
@unlink($siteIconFile->getRealPath());
$siteIconFile = new File($tempFilePath, true);
}
$icoTempFilePath = WRITEPATH . 'temp/img_favicon.ico';
// generate ico
$ico_lib = new PHP_ICO();
$ico_lib->add_image($siteIconFile->getRealPath(), [[32, 32], [64, 64]]);
$ico_lib->save_ico($icoTempFilePath);
// generate random hash to use as a suffix to renew browser cache
$randomHash = substr(bin2hex(random_bytes(18)), 0, 8);
// generate ico
$ico_lib = new PHP_ICO();
$ico_lib->add_image(media_path_absolute('/site/icon.png'), [[32, 32], [64, 64]]);
$ico_lib->save_ico(media_path_absolute("/site/favicon.{$randomHash}.ico"));
// save ico
$fileManager->save(new File($icoTempFilePath, true), "site/favicon.{$randomHash}.ico");
// resize original to needed sizes
foreach ([64, 180, 192, 512] as $size) {
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
service('image')
->withFile(media_path_absolute('/site/icon.png'))
->withFile($siteIconFile->getRealPath())
->resize($size, $size)
->save(media_path_absolute("/site/icon-{$size}.{$randomHash}.png"));
->save($tempFilePath);
// save sizes to
$fileManager->save(new File($tempFilePath), "site/icon-{$size}.{$randomHash}.png");
}
// save original as png
$fileManager->save($siteIconFile, 'site/icon.png');
service('settings')
->set('App.siteIcon', [
'ico' => '/' . media_path("/site/favicon.{$randomHash}.ico"),
'64' => '/' . media_path("/site/icon-64.{$randomHash}.png"),
'180' => '/' . media_path("/site/icon-180.{$randomHash}.png"),
'192' => '/' . media_path("/site/icon-192.{$randomHash}.png"),
'512' => '/' . media_path("/site/icon-512.{$randomHash}.png"),
'ico' => "site/favicon.{$randomHash}.ico",
'64' => "site/icon-64.{$randomHash}.png",
'180' => "site/icon-180.{$randomHash}.png",
'192' => "site/icon-192.{$randomHash}.png",
'512' => "site/icon-512.{$randomHash}.png",
]);
}
@ -107,9 +120,11 @@ class SettingsController extends BaseController
public function deleteIcon(): RedirectResponse
{
helper(['filesystem', 'media']);
// delete site folder in media
delete_files(media_path_absolute('/site'));
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
// delete site folder
$fileManager->deleteAll('site');
service('settings')
->forget('App.siteIcon');

View File

@ -30,11 +30,11 @@ class Fediverse extends BaseConfig
* Default avatar and cover images
* --------------------------------------------------------------------
*/
public string $defaultAvatarImagePath = 'media/avatar-default.jpg';
public string $defaultAvatarImagePath = 'avatar-default.jpg';
public string $defaultAvatarImageMimetype = 'image/jpeg';
public string $defaultCoverImagePath = 'media/banner-default.jpg';
public string $defaultCoverImagePath = 'banner-default.jpg';
public string $defaultCoverImageMimetype = 'image/jpeg';

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
$routes = service('routes');
$routes->get('static/(:any)', 'MediaController::serve/$1', [
'as' => 'media-serve',
'namespace' => 'Modules\Media\Controllers',
]);

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Media\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\Response;
use Modules\Media\FileManagers\FileManagerInterface;
class MediaController extends Controller
{
public function serve(string ...$key): Response
{
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
return $fileManager->serve(implode('/', $key));
}
}

View File

@ -12,9 +12,6 @@ namespace Modules\Media\Entities;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use Modules\Media\FileManagers\FileManagerInterface;
use Modules\Media\FileManagers\FS;
use Modules\Media\FileManagers\S3;
use Modules\Media\Models\MediaModel;
/**
@ -58,28 +55,13 @@ class BaseMedia extends Entity
'updated_by' => 'integer',
];
protected FileManagerInterface $fileManager;
/**
* @param array<string, mixed>|null $data
* @param 'fs'|'s3'|null $fileManager
*/
public function __construct(array $data = null, string $fileManager = null)
public function __construct(array $data = null)
{
parent::__construct($data);
if ($fileManager !== null) {
$this->fileManager = match ($fileManager) {
'fs' => new FS(config('Media')),
's3' => new S3(config('Media'))
};
} else {
/** @var FileManagerInterface $fileManagerService */
$fileManagerService = service('file_manager');
$this->fileManager = $fileManagerService;
}
$this->initFileProperties();
}
@ -92,7 +74,7 @@ class BaseMedia extends Entity
'extension' => $extension,
] = pathinfo($this->file_key);
$this->attributes['file_url'] = $this->fileManager->getUrl($this->file_key);
$this->attributes['file_url'] = service('file_manager')->getUrl($this->file_key);
$this->attributes['file_name'] = $filename;
$this->attributes['file_directory'] = $dirname;
$this->attributes['file_extension'] = $extension;
@ -120,14 +102,14 @@ class BaseMedia extends Entity
return false;
}
$this->attributes['file_key'] = $this->fileManager->save($this->attributes['file'], $this->file_key);
$this->attributes['file_key'] = service('file_manager')->save($this->attributes['file'], $this->file_key);
return true;
}
public function deleteFile(): bool
{
return $this->fileManager->delete($this->file_key);
return service('file_manager')->delete($this->file_key);
}
public function rename(): bool
@ -143,7 +125,7 @@ class BaseMedia extends Entity
return false;
}
if (! $this->fileManager->rename($this->file_key, $newFileKey)) {
if (! service('file_manager')->rename($this->file_key, $newFileKey)) {
$db->transRollback();
return false;
}

View File

@ -42,14 +42,12 @@ class Image extends BaseMedia
{
helper('filesystem');
$fileKeyWithoutExt = path_without_ext($this->file_key);
foreach ($this->sizes as $name => $size) {
$extension = array_key_exists('extension', $size) ? $size['extension'] : $this->file_extension;
$mimetype = array_key_exists('mimetype', $size) ? $size['mimetype'] : $this->file_mimetype;
$this->{$name . '_key'} = $fileKeyWithoutExt . '_' . $name . '.' . $extension;
$this->{$name . '_url'} = $this->fileManager->getUrl($this->{$name . '_key'});
$this->{$name . '_key'} = change_file_path($this->file_key, '_' . $name, $extension);
$this->{$name . '_url'} = service('file_manager')->getUrl($this->{$name . '_key'});
$this->{$name . '_mimetype'} = $mimetype;
}
@ -126,7 +124,8 @@ class Image extends BaseMedia
// download image temporarily to generate sizes from
$tempImagePath = (string) tempnam(WRITEPATH . 'temp', 'img_');
$imageContent = $this->fileManager->getFileContents($this->file_key);
$imageContent = (string) service('file_manager')
->getFileContents($this->file_key);
file_put_contents($tempImagePath, $imageContent);
$this->attributes['file'] = new File($tempImagePath, true);
@ -144,7 +143,7 @@ class Image extends BaseMedia
$newImage = new File($tempFilePath, true);
$this->fileManager
service('file_manager')
->save($newImage, $this->{$name . '_key'});
}
@ -159,7 +158,7 @@ class Image extends BaseMedia
foreach (array_keys($this->sizes) as $name) {
$pathProperty = $name . '_key';
if (! $this->fileManager->delete($this->{$pathProperty})) {
if (! service('file_manager')->delete($this->{$pathProperty})) {
return false;
}
}

View File

@ -29,7 +29,7 @@ class Transcript extends BaseMedia
helper('media');
$this->json_key = $this->file_metadata['json_key'];
$this->json_url = $this->fileManager
$this->json_url = service('file_manager')
->getUrl($this->json_key);
}
}
@ -42,12 +42,8 @@ class Transcript extends BaseMedia
helper('filesystem');
$fileKeyWithoutExt = path_without_ext($this->file_key);
$jsonfileKey = $fileKeyWithoutExt . '.json';
// set metadata (generated json file path)
$this->json_key = $jsonfileKey;
$this->json_key = change_file_path($this->file_key, '', 'json');
$metadata['json_key'] = $this->json_key;
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
@ -71,7 +67,7 @@ class Transcript extends BaseMedia
}
if ($this->json_key) {
return $this->fileManager->delete($this->json_key);
return service('file_manager')->delete($this->json_key);
}
return true;
@ -96,7 +92,7 @@ class Transcript extends BaseMedia
$newTranscriptJson = new File($tempFilePath, true);
$this->fileManager
service('file_manager')
->save($newTranscriptJson, $this->json_key);
return true;

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Modules\Media\FileManagers;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Response;
use Exception;
use Modules\Media\Config\Media as MediaConfig;
@ -27,7 +28,7 @@ class FS implements FileManagerInterface
$path = $path . '.' . $extension;
}
$mediaRoot = media_path_absolute();
$mediaRoot = $this->media_path_absolute();
if (! file_exists(dirname($mediaRoot . '/' . $path))) {
mkdir(dirname($mediaRoot . '/' . $path), 0777, true);
@ -51,7 +52,7 @@ class FS implements FileManagerInterface
{
helper('media');
return @unlink(media_path_absolute($key));
return @unlink($this->media_path_absolute($key));
}
public function getUrl(string $key): string
@ -70,21 +71,21 @@ class FS implements FileManagerInterface
{
helper('media');
return rename(media_path_absolute($oldKey), media_path_absolute($newKey));
return rename($this->media_path_absolute($oldKey), $this->media_path_absolute($newKey));
}
public function getFileContents(string $key): string
public function getFileContents(string $key): string|false
{
helper('media');
return (string) file_get_contents(media_path_absolute($key));
return file_get_contents($this->media_path_absolute($key));
}
public function getFileInput(string $key): string
{
helper('media');
return media_path_absolute($key);
return $this->media_path_absolute($key);
}
public function deletePodcastImageSizes(string $podcastHandle): bool
@ -113,12 +114,12 @@ class FS implements FileManagerInterface
if ($pattern === '*') {
helper('filesystem');
return delete_files(media_path_absolute($prefix), true);
return delete_files($this->media_path_absolute($prefix), true);
}
$prefix = rtrim($prefix, '/') . '/';
$imagePaths = glob(media_path_absolute($prefix . $pattern));
$imagePaths = glob($this->media_path_absolute($prefix . $pattern));
if (! $imagePaths) {
return true;
@ -135,6 +136,28 @@ class FS implements FileManagerInterface
{
helper('media');
return is_really_writable(media_path_absolute());
return is_really_writable($this->media_path_absolute());
}
public function serve(string $key): Response
{
return redirect()->to($this->getUrl($key));
}
/**
* Prefixes the absolute storage directory to the media path of a given uri
*
* @param string|string[] $uri URI string or array of URI segments
*/
private function media_path_absolute(string | array $uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
return config('Media')->storage . '/' . config('Media')->root . '/' . $uri;
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Modules\Media\FileManagers;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Response;
interface FileManagerInterface
{
@ -16,7 +17,7 @@ interface FileManagerInterface
public function rename(string $oldKey, string $newKey): bool;
public function getFileContents(string $key): string;
public function getFileContents(string $key): string|false;
public function getFileInput(string $key): string;
@ -27,4 +28,6 @@ interface FileManagerInterface
public function deleteAll(string $prefix, string $pattern = '*'): bool;
public function isHealthy(): bool;
public function serve(string $key): Response;
}

View File

@ -6,8 +6,9 @@ namespace Modules\Media\FileManagers;
use Aws\Credentials\Credentials;
use Aws\S3\S3Client;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\Response;
use Exception;
use Modules\Media\Config\Media as MediaConfig;
@ -35,6 +36,7 @@ class S3 implements FileManagerInterface
'Bucket' => $this->config->s3['bucket'],
'Key' => $this->prefixKey($key),
'SourceFile' => $file,
'ContentType' => $file->getMimeType(),
]);
} catch (Exception) {
return false;
@ -62,16 +64,7 @@ class S3 implements FileManagerInterface
public function getUrl(string $key): string
{
$url = new URI((string) $this->config->s3['endpoint']);
if ($this->config->s3['pathStyleEndpoint'] === true) {
$url->setPath($this->config->s3['bucket'] . '/' . $this->prefixKey($key));
return (string) $url;
}
$url->setHost($this->config->s3['bucket'] . '.' . $url->getHost());
$url->setPath($this->prefixKey($key));
return (string) $url;
return url_to('media-serve', $key);
}
public function rename(string $oldKey, string $newKey): bool
@ -91,12 +84,16 @@ class S3 implements FileManagerInterface
return $this->delete($oldKey);
}
public function getFileContents(string $key): string
public function getFileContents(string $key): string|false
{
$result = $this->s3->getObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $this->prefixKey($key),
]);
try {
$result = $this->s3->getObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $this->prefixKey($key),
]);
} catch (Exception) {
return false;
}
return (string) $result->get('Body');
}
@ -186,6 +183,31 @@ class S3 implements FileManagerInterface
return true;
}
public function serve(string $key): Response
{
$response = service('response');
try {
$result = $this->s3->getObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $this->prefixKey($key),
]);
} catch (Exception) {
throw new PageNotFoundException();
}
// Remove Cache-Control header before redefining it
header_remove('Cache-Control');
return $response->setCache([
'max-age' => DECADE,
'last-modified' => $result->get('LastModified'),
'etag' => $result->get('ETag'),
'public' => true,
])->setContentType($result->get('ContentType'))
->setBody((string) $result->get('Body')->getContents());
}
private function prefixKey(string $key): string
{
if ($this->config->s3['keyPrefix'] === '') {

View File

@ -8,20 +8,13 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
if (! function_exists('path_without_ext')) {
function path_without_ext(string $path): string
if (! function_exists('add_suffix_to_path')) {
function change_file_path(string $path, string $suffix = '', ?string $newExtension = null): string
{
$fileKeyInfo = pathinfo($path);
if ($fileKeyInfo['dirname'] === '.' && ! str_starts_with($path, '.')) {
return $fileKeyInfo['filename'];
if ($newExtension === null) {
$newExtension = pathinfo($path, PATHINFO_EXTENSION);
}
if ($fileKeyInfo['dirname'] === '/') {
return '/' . $fileKeyInfo['filename'];
}
return implode('/', [$fileKeyInfo['dirname'], $fileKeyInfo['filename']]);
return preg_replace('~\.[^.]+$~', '', $path) . $suffix . '.' . $newExtension;
}
}

View File

@ -63,34 +63,3 @@ if (! function_exists('download_file')) {
return new File($tmpfileKey);
}
}
if (! function_exists('media_path')) {
/**
* Prefixes the root media path to a given uri
*
* @param string|string[] $uri URI string or array of URI segments
*/
function media_path(string | array $uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
return config('Media')->root . '/' . $uri;
}
}
if (! function_exists('media_path_absolute')) {
/**
* Prefixes the absolute storage directory to the media path of a given uri
*
* @param string|string[] $uri URI string or array of URI segments
*/
function media_path_absolute(string | array $uri = ''): string
{
return config('Media')->storage . '/' . media_path($uri);
}
}

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>

View File

@ -15,9 +15,8 @@ $isEpisodeArea = isset($podcast) && isset($episode);
<title><?= $this->renderSection('title') ?> | Castopod Admin</title>
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />

View File

@ -58,7 +58,7 @@
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $person->id) .
'"><img src="' . $person->avatar->thumbnail_url . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 rounded-full aspect-square" loading="lazy" /></a>' .
'"><img src="' . get_avatar_url($person, 'thumbnail') . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 rounded-full aspect-square" loading="lazy" /></a>' .
'<div class="flex flex-col ml-3">' .
esc($person->full_name) .
implode(

View File

@ -2,7 +2,7 @@
<a href="<?= route_to('person-view', $person->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient mix-blend-multiply"></div>
<div class="w-full h-full overflow-hidden bg-header">
<img alt="<?= esc($person->full_name) ?>" src="<?= $person->avatar->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" loading="lazy" />
<img alt="<?= esc($person->full_name) ?>" src="<?= get_avatar_url($person, 'medium') ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" loading="lazy" />
</div>
<div class="absolute z-20">
<h2 class="px-4 py-2 font-semibold leading-tight"><?= esc($person->full_name) ?></h2>

View File

@ -17,7 +17,7 @@
<div class="flex flex-wrap gap-2">
<img
src="<?= $person->avatar->medium_url ?>"
src="<?= get_avatar_url($person, 'medium') ?>"
alt="<?= esc($person->full_name) ?>"
class="object-cover w-full max-w-xs rounded aspect-square"
loading="lazy"

View File

@ -25,7 +25,7 @@
<?php if ($podcast->banner_id !== null): ?>
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast focus:ring-accent top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>" data-tooltip="bottom"><?= icon('delete-bin') ?></a>
<?php endif; ?>
<img src="<?= $podcast->banner->small_url ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
<img src="<?= get_podcast_banner_url($podcast, 'small') ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
<div class="flex px-4 py-2">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= esc($podcast->title) ?>"
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" loading="lazy" />

View File

@ -55,7 +55,7 @@
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $person->id) .
'"><img src="' . $person->avatar->thumbnail_url . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 h-16 rounded-full aspect-square" loading="lazy" /></a>' .
'"><img src="' . get_avatar_url($person, 'thumbnail') . '" alt="' . esc($person->full_name) . '" class="object-cover w-16 h-16 rounded-full aspect-square" loading="lazy" /></a>' .
'<div class="flex flex-col ml-3">' .
esc($person->full_name) .
implode(

View File

@ -46,7 +46,7 @@
<?php if (config('App')->siteIcon['ico'] !== service('settings')->get('App.siteIcon')['ico']): ?>
<div class="relative ml-2">
<a href="<?= route_to('settings-instance-delete-icon') ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast -top-3 -right-3 focus:ring-accent" title="<?= lang('Settings.instance.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin') ?></a>
<img src="<?= service('settings')->get('App.siteIcon')['64'] ?>" alt="<?= esc(service('settings')->get('App.siteName')) ?> Favicon" class="w-10 h-10 aspect-square" loading="lazy" />
<img src="<?= get_site_icon_url('64') ?>" alt="<?= esc(service('settings')->get('App.siteName')) ?> Favicon" class="w-10 h-10 aspect-square" loading="lazy" />
</div>
<?php endif; ?>
</div>
@ -62,7 +62,7 @@
<Forms.Section
title="<?= lang('Settings.images.title') ?>"
subtitle="<?= lang('Settings.images.subtitle') ?>" >
subtitle="<?= lang('Settings.images.subtitle') ?>">
<Button variant="primary" type="submit" iconLeft="refresh"><?= lang('Settings.images.regenerate') ?></Button>

View File

@ -17,7 +17,7 @@
<div class="flex flex-col items-start p-4 gap-y-4">
<?php foreach ($persons as $person): ?>
<div class="flex gap-x-2">
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-10 rounded-full bg-header aspect-square" loading="lazy" />
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-10 rounded-full bg-header aspect-square" loading="lazy" />
<div class="flex flex-col">
<h4 class="text-sm font-semibold">
<?php if ($person->information_url): ?>

View File

@ -9,9 +9,8 @@
<meta name="description" content="<?= esc(
$episode->description,
) ?>" />
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
<?= service('vite')
->asset('styles/index.css', 'css') ?>

View File

@ -7,9 +7,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
@ -78,7 +77,7 @@
</div>
</nav>
<header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-accent-base/75 gap-y-4">
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= $episode->podcast->banner->small_url ?>');"></div>
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= get_podcast_banner_url($episode->podcast, 'small') ?>');"></div>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div>
<div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row">
<div class="relative flex-shrink-0">
@ -97,7 +96,7 @@
<span class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($episode->persons as $person): ?>
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
<?php $i++;
if ($i === 3) {
break;

View File

@ -6,9 +6,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>

View File

@ -6,9 +6,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>

View File

@ -11,9 +11,8 @@
->get('App.siteName')),
]) ?>"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('webmanifest') ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>

View File

@ -7,9 +7,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
@ -43,7 +42,7 @@
</div>
<?php endif; ?>
<header class="min-h-[200px] relative z-50 flex flex-col-reverse justify-between w-full gap-x-2 col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= $podcast->banner->medium_url ?>');">
<header class="min-h-[200px] relative z-50 flex flex-col-reverse justify-between w-full gap-x-2 col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= get_podcast_banner_url($podcast, 'medium') ?>');">
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient mix-blend-multiply"></div>
<div class="z-10 flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= esc($podcast->title) ?>" class="h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated aspect-square" loading="lazy" />

View File

@ -21,7 +21,7 @@
<span class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($podcast->persons as $person): ?>
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 -ml-4 border-2 rounded-full aspect-square bg-header border-background-base last:ml-0" loading="lazy" />
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 -ml-4 border-2 rounded-full aspect-square bg-header border-background-base last:ml-0" loading="lazy" />
<?php $i++;
if ($i === 3) {
break;

View File

@ -7,9 +7,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($actor->podcast->handle)) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
@ -38,7 +37,7 @@
'Fediverse.follow.subtitle',
) ?></h1>
<div class="flex flex-col w-full max-w-xs -mt-24 overflow-hidden shadow bg-elevated rounded-xl">
<img src="<?= $actor->podcast->banner->small_url ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
<img src="<?= get_podcast_banner_url($actor->podcast, 'small') ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
<div class="flex px-4 py-2">
<img src="<?= $actor->avatar_image_url ?>" alt="<?= esc($actor->display_name) ?>"
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-background-elevated aspect-square" loading="lazy" />

View File

@ -7,9 +7,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>
@ -70,7 +69,7 @@
</form>
</div>
<header class="relative flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= $podcast->banner->medium_url ?>');">
<header class="relative flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-header aspect-[3/1]" style="background-image: url('<?= get_podcast_banner_url($podcast, 'medium') ?>');">
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient mix-blend-multiply"></div>
<div class="flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= esc($podcast->title) ?>" class="z-[45] h-24 rounded-full sm:h-28 md:h-36 ring-3 ring-background-elevated aspect-square" loading="lazy" />

View File

@ -5,9 +5,8 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/x-icon" href="<?= service('settings')
->get('App.siteIcon')['ico'] ?>" />
<link rel="apple-touch-icon" href="<?= service('settings')->get('App.siteIcon')['180'] ?>">
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($post->actor->podcast->handle)) ?>">
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
<script>