Fix image links

This commit is contained in:
DariusIII
2026-07-16 16:45:14 +02:00
parent b27fb8d86b
commit ba7cef9b3c
21 changed files with 190 additions and 69 deletions
+72 -22
View File
@@ -508,6 +508,60 @@ if (! function_exists('release_flag')) {
}
}
if (! function_exists('resolveImageAssetFilename')) {
/**
* Resolve the real on-disk extension before publishing an image URL.
*
* @param list<string> $alternateBasenames
*/
function resolveImageAssetFilename(string $type, string $basename, array $alternateBasenames = []): ?string
{
if (preg_match('/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/D', $type) !== 1
|| preg_match('/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/D', $basename) !== 1) {
return null;
}
$configuredRoot = config('nntmux_settings.covers_path');
$roots = array_values(array_unique(array_filter([
is_string($configuredRoot) && $configuredRoot !== '' ? rtrim($configuredRoot, '/\\') : null,
storage_path('covers'),
public_path('covers'),
], static fn (mixed $root): bool => is_string($root) && $root !== '')));
$basenames = array_values(array_unique([$basename, ...$alternateBasenames]));
foreach (['webp', 'jpg', 'jpeg'] as $extension) {
foreach ($basenames as $candidateBasename) {
if (preg_match('/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/D', $candidateBasename) !== 1) {
continue;
}
foreach ($roots as $root) {
$path = $root.DIRECTORY_SEPARATOR.$type.DIRECTORY_SEPARATOR.$candidateBasename.'.'.$extension;
if (is_file($path) && is_readable($path)) {
return $candidateBasename.'.'.$extension;
}
}
}
}
return null;
}
}
if (! function_exists('getImageAssetUrl')) {
/**
* Return a URL containing the extension of the image that actually exists.
*
* @param list<string> $alternateBasenames
*/
function getImageAssetUrl(string $type, string $basename, ?string $fallbackUrl = null, array $alternateBasenames = []): ?string
{
$filename = resolveImageAssetFilename($type, $basename, $alternateBasenames);
return $filename === null ? $fallbackUrl : url("/covers/{$type}/{$filename}");
}
}
if (! function_exists('getReleaseCover')) {
/**
* Get the cover image URL for a release based on its type and ID
@@ -567,11 +621,17 @@ if (! function_exists('getReleaseCover')) {
}
if ($coverType && $coverId) {
if (in_array($coverType, ['movies', 'anime'], true)) {
return url("/covers/{$coverType}/{$coverId}-cover.webp");
}
$basename = in_array($coverType, ['movies', 'anime'], true)
? $coverId.'-cover'
: (string) $coverId;
$alternateBasenames = $coverType === 'anime' ? [(string) $coverId] : [];
return url("/covers/{$coverType}/{$coverId}.webp");
return getImageAssetUrl(
$coverType,
$basename,
asset('assets/images/no-cover.png'),
$alternateBasenames
) ?? asset('assets/images/no-cover.png');
}
// Return placeholder image if no cover type/ID found
@@ -832,15 +892,12 @@ if (! function_exists('streamSslContextOptions')) {
if (! function_exists('getCoverURL')) {
/**
* Get cover URL for a release. Uses a short-lived in-memory cache to avoid
* repeated filesystem file_exists() calls for the same cover during a single request.
* Get the relative cover URL using the extension that exists on disk.
*
* @param array<string, mixed> $options
*/
function getCoverURL(array $options = []): string
{
static $coverCache = [];
$defaults = [
'id' => null,
'suffix' => '-cover.webp',
@@ -852,23 +909,16 @@ if (! function_exists('getCoverURL')) {
if (! empty($options['id']) && \in_array(
$options['type'],
['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'video', 'xxx'],
['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'tvshows', 'video', 'xxx'],
false
)
) {
$fileSpec = sprintf($fileSpecTemplate, $options['type'], $options['id'], $options['suffix']);
$cacheKey = $options['type'].':'.$options['id'];
if (! isset($coverCache[$cacheKey])) {
$canonicalPath = storage_path('covers/').$fileSpec;
$legacyPath = preg_replace('/\.webp$/i', '.jpg', $canonicalPath);
$coverCache[$cacheKey] = file_exists($canonicalPath)
|| (is_string($legacyPath) && file_exists($legacyPath));
}
if (! $coverCache[$cacheKey]) {
$fileSpec = sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
}
$suffix = preg_replace('/\.(?:webp|jpe?g)$/i', '', (string) $options['suffix']);
$basename = (string) $options['id'].(is_string($suffix) ? $suffix : '');
$filename = resolveImageAssetFilename((string) $options['type'], $basename);
$fileSpec = $filename === null
? sprintf($fileSpecTemplate, $options['type'], 'no-cover', '.jpg')
: $options['type'].'/'.$filename;
}
return $fileSpec;
+3 -2
View File
@@ -664,15 +664,16 @@ class XML_Response
$column = 'consoleinfo_id';
break;
case ! empty($this->release->bo_cover):
$dir = 'books';
$dir = 'book';
$column = 'bookinfo_id';
break;
}
if (isset($dir, $column)) {
$dcov = ($dir === 'movies' ? '-cover' : '');
$filename = resolveImageAssetFilename($dir, $this->release->$column.$dcov) ?? 'no-cover.jpg';
$this->cdata .=
"\t<img style=\"margin-left:10px;margin-bottom:10px;float:right;\" ".
"src=\"{$this->server['server']['url']}/covers/{$dir}/{$this->release->$column}{$dcov}.webp\" ".
"src=\"{$this->server['server']['url']}/covers/{$dir}/{$filename}\" ".
"width=\"120\" alt=\"{$this->release->searchname}\" />\n";
}
$size = human_filesize($this->release->size);
+6 -8
View File
@@ -68,13 +68,11 @@ class AnidbResource extends JsonResource
}
// Otherwise construct the local path
$picturePath = storage_path('covers/anime/'.$this->anidbid.'-cover.webp');
$legacyPath = storage_path('covers/anime/'.$this->anidbid.'-cover.jpg');
$oldLegacyPath = storage_path('covers/anime/'.$this->anidbid.'.jpg');
if (file_exists($picturePath) || file_exists($legacyPath) || file_exists($oldLegacyPath)) {
return url('/covers/anime/'.$this->anidbid.'-cover.webp');
}
return null;
return getImageAssetUrl(
'anime',
$this->anidbid.'-cover',
null,
[(string) $this->anidbid]
);
}
}
+1 -7
View File
@@ -52,12 +52,6 @@ class BookResource extends JsonResource
return null;
}
$coverPath = storage_path('covers/book/'.$this->id.'.webp');
$legacyPath = storage_path('covers/book/'.$this->id.'.jpg');
if (file_exists($coverPath) || file_exists($legacyPath)) {
return url('/covers/book/'.$this->id.'.webp');
}
return null;
return getImageAssetUrl('book', (string) $this->id);
}
}
+2 -2
View File
@@ -58,7 +58,7 @@ class MovieResource extends JsonResource
return null;
}
return url('/covers/movies/'.$this->imdbid.'-cover.webp');
return getImageAssetUrl('movies', (string) $this->imdbid.'-cover');
}
/**
@@ -70,6 +70,6 @@ class MovieResource extends JsonResource
return null;
}
return url('/covers/movies/'.$this->imdbid.'-backdrop.webp');
return getImageAssetUrl('movies', (string) $this->imdbid.'-backdrop');
}
}
+6 -1
View File
@@ -143,7 +143,12 @@ class AnidbInfo extends Model
// Otherwise construct the local path
if ($this->hasPictureImage()) {
return url('/covers/anime/'.$this->anidbid.'-cover.webp');
return getImageAssetUrl(
'anime',
$this->anidbid.'-cover',
null,
[(string) $this->anidbid]
);
}
return null;
+1 -1
View File
@@ -111,6 +111,6 @@ class BookInfo extends Model
return null;
}
return url('/covers/book/'.$this->id.'.webp');
return getImageAssetUrl('book', (string) $this->id);
}
}
+10 -10
View File
@@ -9,8 +9,8 @@ function buildImageUrl(guid, type) {
return '/covers/' + (type || 'preview') + '/' + guid + '_thumb.webp';
}
function prefetchImage(guid, type) {
const url = buildImageUrl(guid, type);
function prefetchImage(guid, type, resolvedUrl) {
const url = resolvedUrl || buildImageUrl(guid, type);
if (!prefetchedUrls.has(url)) {
const img = new Image();
img.src = url;
@@ -25,10 +25,10 @@ Alpine.data('previewModal', () => ({
imageError: false,
imageLoaded: false,
show(guid, type) {
show(guid, type, resolvedUrl) {
type = type || 'preview';
this.title = type === 'sample' ? 'Sample Image' : 'Preview Image';
const newUrl = buildImageUrl(guid, type);
const newUrl = resolvedUrl || buildImageUrl(guid, type);
if (this.imageUrl === newUrl) {
this.open = true;
@@ -64,18 +64,18 @@ Alpine.data('previewModal', () => ({
document.addEventListener('click', function(e) {
const preview = e.target.closest('.preview-badge');
if (preview) { e.preventDefault(); self.show(preview.dataset.guid, 'preview'); return; }
if (preview) { e.preventDefault(); self.show(preview.dataset.guid, 'preview', preview.dataset.imageUrl); return; }
const sample = e.target.closest('.sample-badge');
if (sample) { e.preventDefault(); self.show(sample.dataset.guid, 'sample'); return; }
if (sample) { e.preventDefault(); self.show(sample.dataset.guid, 'sample', sample.dataset.imageUrl); return; }
if (e.target.closest('[data-close-preview-modal]')) { e.preventDefault(); self.close(); }
});
// Prefetch on hover so the image is cached before click
document.addEventListener('mouseover', function(e) {
const preview = e.target.closest('.preview-badge');
if (preview) { prefetchImage(preview.dataset.guid, 'preview'); return; }
if (preview) { prefetchImage(preview.dataset.guid, 'preview', preview.dataset.imageUrl); return; }
const sample = e.target.closest('.sample-badge');
if (sample) { prefetchImage(sample.dataset.guid, 'sample'); }
if (sample) { prefetchImage(sample.dataset.guid, 'sample', sample.dataset.imageUrl); }
});
document.addEventListener('keydown', function(e) {
@@ -91,9 +91,9 @@ Alpine.data('previewModal', () => ({
const type = el.classList.contains('sample-badge') ? 'sample' : 'preview';
const guid = el.dataset.guid;
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(function() { prefetchImage(guid, type); });
requestIdleCallback(function() { prefetchImage(guid, type, el.dataset.imageUrl); });
} else {
setTimeout(function() { prefetchImage(guid, type); }, 200);
setTimeout(function() { prefetchImage(guid, type, el.dataset.imageUrl); }, 200);
}
observer.unobserve(el);
}
+1 -1
View File
@@ -128,7 +128,7 @@
$hasCover = $anime['anidbid'] > 0 && (file_exists(storage_path('covers/anime/' . $anime['anidbid'] . '-cover.webp')) || file_exists(storage_path('covers/anime/' . $anime['anidbid'] . '-cover.jpg')));
@endphp
@if($hasCover)
<img src="{{ url('/covers/anime/' . $anime['anidbid'] . '-cover.webp') }}"
<img src="{{ getImageAssetUrl('anime', $anime['anidbid'] . '-cover', url('/covers/anime/no-cover.jpg'), [(string) $anime['anidbid']]) }}"
alt="{{ $anime['title'] }}"
class="max-w-full h-auto mx-auto rounded shadow-lg"
style="max-height: 400px;">
+1 -1
View File
@@ -64,7 +64,7 @@
$hasCover = $anime->anidbid > 0 && (file_exists(storage_path('covers/anime/' . $anime->anidbid . '-cover.webp')) || file_exists(storage_path('covers/anime/' . $anime->anidbid . '-cover.jpg')));
@endphp
@if($hasCover)
<img src="{{ url('/covers/anime/' . $anime->anidbid . '-cover.webp') }}"
<img src="{{ getImageAssetUrl('anime', $anime->anidbid . '-cover', url('/covers/anime/no-cover.jpg'), [(string) $anime->anidbid]) }}"
alt="{{ $anime->title }}"
class="h-16 w-12 object-cover rounded shadow"
loading="lazy">
+1 -1
View File
@@ -96,7 +96,7 @@
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow duration-200">
<a href="{{ $guid ? url('/details/' . $guid) : '#' }}" class="block relative">
@if(!empty($result->cover))
<img src="{{ url('/covers/book/' . $result->id . '.webp') }}"
<img src="{{ getImageAssetUrl('book', (string) $result->id, asset('assets/images/no-cover.png')) }}"
alt="{{ $result->title }}"
class="w-full h-64 object-cover"
data-fallback-src="{{ url('/images/no-cover.png') }}">
@@ -76,6 +76,7 @@
<button type="button"
class="preview-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
data-image-url="{{ getImageAssetUrl('preview', $result->guid . '_thumb') }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
@@ -84,6 +85,7 @@
<button type="button"
class="sample-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
data-image-url="{{ getImageAssetUrl('sample', $result->guid . '_thumb') }}"
title="View sample image">
<i class="fas fa-images mr-1"></i> Sample
</button>
+10 -4
View File
@@ -127,6 +127,12 @@
@php
$hasPreviewImage = isset($release->haspreview) && $release->haspreview == 1;
$hasSampleImage = isset($release->jpgstatus) && $release->jpgstatus == 1;
$previewImageUrl = $hasPreviewImage
? getImageAssetUrl('preview', $release->guid . '_thumb', asset('assets/images/no-cover.png'))
: null;
$sampleImageUrl = $hasSampleImage
? getImageAssetUrl('sample', $release->guid . '_thumb', asset('assets/images/no-cover.png'))
: null;
@endphp
@if($hasPreviewImage || $hasSampleImage)
@@ -145,8 +151,8 @@
@if($hasPreviewImage)
<!-- Preview image -->
<div>
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ url('/covers/preview/' . $release->guid . '_thumb.webp') }}" data-image-title="Preview Image">
<img src="{{ url('/covers/preview/' . $release->guid . '_thumb.webp') }}"
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ $previewImageUrl }}" data-image-title="Preview Image">
<img src="{{ $previewImageUrl }}"
alt="Preview"
class="detail-gallery-image w-full h-auto rounded-lg"
loading="lazy">
@@ -158,8 +164,8 @@
@if($hasSampleImage)
<!-- Sample image -->
<div>
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ url('/covers/sample/' . $release->guid . '_thumb.webp') }}" data-image-title="Sample Image">
<img src="{{ url('/covers/sample/' . $release->guid . '_thumb.webp') }}"
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ $sampleImageUrl }}" data-image-title="Sample Image">
<img src="{{ $sampleImageUrl }}"
alt="Sample"
class="detail-gallery-image w-full h-auto rounded-lg"
loading="lazy">
@@ -172,6 +172,7 @@
<button type="button"
class="preview-badge inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
data-image-url="{{ getImageAssetUrl('preview', $release->guid . '_thumb') }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
@@ -42,6 +42,7 @@
<button type="button"
class="preview-badge inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
data-image-url="{{ getImageAssetUrl('preview', $release->guid . '_thumb') }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
+3 -3
View File
@@ -22,7 +22,7 @@
<div class="mb-6">
<div class="flex items-center gap-4 mb-4">
<img class="rounded-lg shadow-md w-24 h-auto"
src="{{ url("/covers/movies/{$imdbid}-cover.webp") }}"
src="{{ getImageAssetUrl('movies', $imdbid . '-cover', url('/covers/movies/no-cover.jpg')) }}"
data-fallback-src="{{ url('/covers/movies/no-cover.jpg') }}"
alt="{{ e($movie['title'] ?? '') }}" />
@@ -146,7 +146,7 @@
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition">
<td class="px-4 py-3">
<img class="rounded-lg shadow-sm max-w-[120px]"
src="{{ url('/covers/movies/' . (($movie['cover'] ?? 0) == 1 ? $movie['imdbid'] . '-cover.webp' : 'no-cover.jpg')) }}"
src="{{ ($movie['cover'] ?? 0) == 1 ? getImageAssetUrl('movies', $movie['imdbid'] . '-cover', url('/covers/movies/no-cover.jpg')) : url('/covers/movies/no-cover.jpg') }}"
alt="{{ e($movie['title'] ?? '') }}"/>
</td>
<td class="px-4 py-3">
@@ -212,7 +212,7 @@
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div class="flex gap-3">
<img class="rounded-lg shadow-sm w-20 h-auto shrink-0"
src="{{ url('/covers/movies/' . (($movie['cover'] ?? 0) == 1 ? $movie['imdbid'] . '-cover.webp' : 'no-cover.jpg')) }}"
src="{{ ($movie['cover'] ?? 0) == 1 ? getImageAssetUrl('movies', $movie['imdbid'] . '-cover', url('/covers/movies/no-cover.jpg')) : url('/covers/movies/no-cover.jpg') }}"
alt="{{ e($movie['title'] ?? '') }}"/>
<div class="min-w-0 flex-1">
<a href="{{ url("/Movies?imdb={$movie['imdbid']}") }}" class="text-gray-900 dark:text-gray-100 font-semibold hover:text-blue-600 dark:hover:text-blue-400 transition text-sm">
+2 -2
View File
@@ -79,7 +79,7 @@
<!-- Movie Poster -->
<div class="shrink-0">
<img class="rounded-lg movie-poster-shadow w-32 h-48 object-cover"
src="{{ url('/covers/movies/' . (($movie['cover'] ?? 0) == 1 ? $movie['imdbid'] . '-cover.webp' : 'no-cover.jpg')) }}"
src="{{ ($movie['cover'] ?? 0) == 1 ? getImageAssetUrl('movies', $movie['imdbid'] . '-cover', url('/covers/movies/no-cover.jpg')) : url('/covers/movies/no-cover.jpg') }}"
alt="{{ e($movie['title'] ?? '') }}"/>
</div>
@@ -176,7 +176,7 @@
<div class="flex gap-4 mb-4">
<div class="shrink-0">
<img class="rounded-lg movie-poster-shadow w-24 h-36 object-cover"
src="{{ url('/covers/movies/' . (($movie['cover'] ?? 0) == 1 ? $movie['imdbid'] . '-cover.webp' : 'no-cover.jpg')) }}"
src="{{ ($movie['cover'] ?? 0) == 1 ? getImageAssetUrl('movies', $movie['imdbid'] . '-cover', url('/covers/movies/no-cover.jpg')) : url('/covers/movies/no-cover.jpg') }}"
alt="{{ e($movie['title'] ?? '') }}"/>
</div>
<div class="flex-1 min-w-0">
+1 -2
View File
@@ -22,7 +22,7 @@
<div class="mb-6">
<div class="flex items-center gap-4 mb-4">
<img class="rounded-lg shadow-md w-24 h-auto"
src="{{ url("/covers/tvshows/{$video}_thumb.webp") }}"
src="{{ getImageAssetUrl('tvshows', $video . '_thumb', url('/covers/tvshows/no-cover.jpg')) }}"
data-fallback-src="{{ url('/covers/tvshows/no-cover.jpg') }}"
alt="{{ e($show['title'] ?? '') }}" />
@@ -87,4 +87,3 @@
</div>
</div>
</div>
+1 -1
View File
@@ -49,7 +49,7 @@
<div class="shrink-0 relative">
<a href="{{ route('series', ['id' => $show->id]) }}" class="block">
@if($show->image)
<img src="{{ url('/covers/tvshows/' . $show->id . '.webp') }}" alt="{{ $show->title }}" class="w-full md:w-64 h-96 object-cover" data-fallback-src="{{ url('/covers/tvshows/no-cover.jpg') }}">
<img src="{{ getImageAssetUrl('tvshows', (string) $show->id, url('/covers/tvshows/no-cover.jpg')) }}" alt="{{ $show->title }}" class="w-full md:w-64 h-96 object-cover" data-fallback-src="{{ url('/covers/tvshows/no-cover.jpg') }}">
@else
<div class="w-full md:w-64 h-96 bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<i class="fas fa-tv text-gray-400 text-5xl"></i>
+1 -1
View File
@@ -93,7 +93,7 @@
<div class="lg:col-span-1">
<img class="series-detail-poster w-full h-auto rounded-lg"
alt="{{ $seriestitles ?? '' }} Poster"
src="{{ url('/covers/tvshows/' . $show['id'] . '.webp') }}"/>
src="{{ getImageAssetUrl('tvshows', (string) $show['id'], url('/covers/tvshows/no-cover.jpg')) }}"/>
</div>
<div class="lg:col-span-3">
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">{{ $seriessummary }}</p>
@@ -8,6 +8,35 @@ use Tests\TestCase;
class HelperCoverUrlTest extends TestCase
{
/** @var list<string> */
private array $temporaryCoverFiles = [];
/** @var list<string> */
private array $temporaryCoverDirectories = [];
private mixed $originalCoversPath;
protected function setUp(): void
{
parent::setUp();
$this->originalCoversPath = config('nntmux_settings.covers_path');
}
protected function tearDown(): void
{
foreach ($this->temporaryCoverFiles as $path) {
@unlink($path);
}
foreach (array_reverse($this->temporaryCoverDirectories) as $path) {
@rmdir($path);
}
config(['nntmux_settings.covers_path' => $this->originalCoversPath]);
parent::tearDown();
}
public function test_unzip_gzip_file_returns_uncompressed_contents(): void
{
$path = tempnam(sys_get_temp_dir(), 'nntmux-gzip-');
@@ -35,4 +64,39 @@ class HelperCoverUrlTest extends TestCase
$this->assertStringContainsString('assets/images/no-cover.png', $url);
}
public function test_get_release_cover_emits_legacy_jpeg_extension_when_webp_does_not_exist(): void
{
$id = 987654321;
$path = storage_path("covers/book/{$id}.jpg");
$this->temporaryCoverFiles[] = $path;
if (! is_dir(dirname($path))) {
mkdir(dirname($path), 0777, true);
}
file_put_contents($path, 'legacy jpeg fixture');
$url = getReleaseCover((object) ['bookinfo_id' => $id]);
$this->assertStringEndsWith("/covers/book/{$id}.jpg", $url);
}
public function test_get_release_cover_uses_the_real_extension_in_a_custom_covers_path(): void
{
$id = 987654322;
$root = sys_get_temp_dir().'/nntmux-cover-url-'.bin2hex(random_bytes(6));
$directory = $root.'/book';
$jpegPath = $directory."/{$id}.jpg";
$webpPath = $directory."/{$id}.webp";
$this->temporaryCoverDirectories = [$root, $directory];
$this->temporaryCoverFiles = [$jpegPath, $webpPath];
mkdir($directory, 0777, true);
config(['nntmux_settings.covers_path' => $root]);
file_put_contents($jpegPath, 'legacy jpeg fixture');
$this->assertStringEndsWith("/covers/book/{$id}.jpg", getReleaseCover((object) ['bookinfo_id' => $id]));
file_put_contents($webpPath, 'webp fixture');
$this->assertStringEndsWith("/covers/book/{$id}.webp", getReleaseCover((object) ['bookinfo_id' => $id]));
}
}