mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Fix image links
This commit is contained in:
@@ -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')) {
|
if (! function_exists('getReleaseCover')) {
|
||||||
/**
|
/**
|
||||||
* Get the cover image URL for a release based on its type and ID
|
* 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 ($coverType && $coverId) {
|
||||||
if (in_array($coverType, ['movies', 'anime'], true)) {
|
$basename = in_array($coverType, ['movies', 'anime'], true)
|
||||||
return url("/covers/{$coverType}/{$coverId}-cover.webp");
|
? $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
|
// Return placeholder image if no cover type/ID found
|
||||||
@@ -832,15 +892,12 @@ if (! function_exists('streamSslContextOptions')) {
|
|||||||
|
|
||||||
if (! function_exists('getCoverURL')) {
|
if (! function_exists('getCoverURL')) {
|
||||||
/**
|
/**
|
||||||
* Get cover URL for a release. Uses a short-lived in-memory cache to avoid
|
* Get the relative cover URL using the extension that exists on disk.
|
||||||
* repeated filesystem file_exists() calls for the same cover during a single request.
|
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $options
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
function getCoverURL(array $options = []): string
|
function getCoverURL(array $options = []): string
|
||||||
{
|
{
|
||||||
static $coverCache = [];
|
|
||||||
|
|
||||||
$defaults = [
|
$defaults = [
|
||||||
'id' => null,
|
'id' => null,
|
||||||
'suffix' => '-cover.webp',
|
'suffix' => '-cover.webp',
|
||||||
@@ -852,23 +909,16 @@ if (! function_exists('getCoverURL')) {
|
|||||||
|
|
||||||
if (! empty($options['id']) && \in_array(
|
if (! empty($options['id']) && \in_array(
|
||||||
$options['type'],
|
$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
|
false
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
$fileSpec = sprintf($fileSpecTemplate, $options['type'], $options['id'], $options['suffix']);
|
$suffix = preg_replace('/\.(?:webp|jpe?g)$/i', '', (string) $options['suffix']);
|
||||||
$cacheKey = $options['type'].':'.$options['id'];
|
$basename = (string) $options['id'].(is_string($suffix) ? $suffix : '');
|
||||||
|
$filename = resolveImageAssetFilename((string) $options['type'], $basename);
|
||||||
if (! isset($coverCache[$cacheKey])) {
|
$fileSpec = $filename === null
|
||||||
$canonicalPath = storage_path('covers/').$fileSpec;
|
? sprintf($fileSpecTemplate, $options['type'], 'no-cover', '.jpg')
|
||||||
$legacyPath = preg_replace('/\.webp$/i', '.jpg', $canonicalPath);
|
: $options['type'].'/'.$filename;
|
||||||
$coverCache[$cacheKey] = file_exists($canonicalPath)
|
|
||||||
|| (is_string($legacyPath) && file_exists($legacyPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $coverCache[$cacheKey]) {
|
|
||||||
$fileSpec = sprintf($fileSpecTemplate, $options['type'], 'no', $options['suffix']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $fileSpec;
|
return $fileSpec;
|
||||||
|
|||||||
@@ -664,15 +664,16 @@ class XML_Response
|
|||||||
$column = 'consoleinfo_id';
|
$column = 'consoleinfo_id';
|
||||||
break;
|
break;
|
||||||
case ! empty($this->release->bo_cover):
|
case ! empty($this->release->bo_cover):
|
||||||
$dir = 'books';
|
$dir = 'book';
|
||||||
$column = 'bookinfo_id';
|
$column = 'bookinfo_id';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isset($dir, $column)) {
|
if (isset($dir, $column)) {
|
||||||
$dcov = ($dir === 'movies' ? '-cover' : '');
|
$dcov = ($dir === 'movies' ? '-cover' : '');
|
||||||
|
$filename = resolveImageAssetFilename($dir, $this->release->$column.$dcov) ?? 'no-cover.jpg';
|
||||||
$this->cdata .=
|
$this->cdata .=
|
||||||
"\t<img style=\"margin-left:10px;margin-bottom:10px;float:right;\" ".
|
"\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";
|
"width=\"120\" alt=\"{$this->release->searchname}\" />\n";
|
||||||
}
|
}
|
||||||
$size = human_filesize($this->release->size);
|
$size = human_filesize($this->release->size);
|
||||||
|
|||||||
@@ -68,13 +68,11 @@ class AnidbResource extends JsonResource
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise construct the local path
|
// Otherwise construct the local path
|
||||||
$picturePath = storage_path('covers/anime/'.$this->anidbid.'-cover.webp');
|
return getImageAssetUrl(
|
||||||
$legacyPath = storage_path('covers/anime/'.$this->anidbid.'-cover.jpg');
|
'anime',
|
||||||
$oldLegacyPath = storage_path('covers/anime/'.$this->anidbid.'.jpg');
|
$this->anidbid.'-cover',
|
||||||
if (file_exists($picturePath) || file_exists($legacyPath) || file_exists($oldLegacyPath)) {
|
null,
|
||||||
return url('/covers/anime/'.$this->anidbid.'-cover.webp');
|
[(string) $this->anidbid]
|
||||||
}
|
);
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ class BookResource extends JsonResource
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$coverPath = storage_path('covers/book/'.$this->id.'.webp');
|
return getImageAssetUrl('book', (string) $this->id);
|
||||||
$legacyPath = storage_path('covers/book/'.$this->id.'.jpg');
|
|
||||||
if (file_exists($coverPath) || file_exists($legacyPath)) {
|
|
||||||
return url('/covers/book/'.$this->id.'.webp');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class MovieResource extends JsonResource
|
|||||||
return null;
|
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url('/covers/movies/'.$this->imdbid.'-backdrop.webp');
|
return getImageAssetUrl('movies', (string) $this->imdbid.'-backdrop');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,12 @@ class AnidbInfo extends Model
|
|||||||
|
|
||||||
// Otherwise construct the local path
|
// Otherwise construct the local path
|
||||||
if ($this->hasPictureImage()) {
|
if ($this->hasPictureImage()) {
|
||||||
return url('/covers/anime/'.$this->anidbid.'-cover.webp');
|
return getImageAssetUrl(
|
||||||
|
'anime',
|
||||||
|
$this->anidbid.'-cover',
|
||||||
|
null,
|
||||||
|
[(string) $this->anidbid]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -111,6 +111,6 @@ class BookInfo extends Model
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url('/covers/book/'.$this->id.'.webp');
|
return getImageAssetUrl('book', (string) $this->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ function buildImageUrl(guid, type) {
|
|||||||
return '/covers/' + (type || 'preview') + '/' + guid + '_thumb.webp';
|
return '/covers/' + (type || 'preview') + '/' + guid + '_thumb.webp';
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefetchImage(guid, type) {
|
function prefetchImage(guid, type, resolvedUrl) {
|
||||||
const url = buildImageUrl(guid, type);
|
const url = resolvedUrl || buildImageUrl(guid, type);
|
||||||
if (!prefetchedUrls.has(url)) {
|
if (!prefetchedUrls.has(url)) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = url;
|
img.src = url;
|
||||||
@@ -25,10 +25,10 @@ Alpine.data('previewModal', () => ({
|
|||||||
imageError: false,
|
imageError: false,
|
||||||
imageLoaded: false,
|
imageLoaded: false,
|
||||||
|
|
||||||
show(guid, type) {
|
show(guid, type, resolvedUrl) {
|
||||||
type = type || 'preview';
|
type = type || 'preview';
|
||||||
this.title = type === 'sample' ? 'Sample Image' : 'Preview Image';
|
this.title = type === 'sample' ? 'Sample Image' : 'Preview Image';
|
||||||
const newUrl = buildImageUrl(guid, type);
|
const newUrl = resolvedUrl || buildImageUrl(guid, type);
|
||||||
|
|
||||||
if (this.imageUrl === newUrl) {
|
if (this.imageUrl === newUrl) {
|
||||||
this.open = true;
|
this.open = true;
|
||||||
@@ -64,18 +64,18 @@ Alpine.data('previewModal', () => ({
|
|||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
const preview = e.target.closest('.preview-badge');
|
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');
|
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(); }
|
if (e.target.closest('[data-close-preview-modal]')) { e.preventDefault(); self.close(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prefetch on hover so the image is cached before click
|
// Prefetch on hover so the image is cached before click
|
||||||
document.addEventListener('mouseover', function(e) {
|
document.addEventListener('mouseover', function(e) {
|
||||||
const preview = e.target.closest('.preview-badge');
|
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');
|
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) {
|
document.addEventListener('keydown', function(e) {
|
||||||
@@ -91,9 +91,9 @@ Alpine.data('previewModal', () => ({
|
|||||||
const type = el.classList.contains('sample-badge') ? 'sample' : 'preview';
|
const type = el.classList.contains('sample-badge') ? 'sample' : 'preview';
|
||||||
const guid = el.dataset.guid;
|
const guid = el.dataset.guid;
|
||||||
if (typeof requestIdleCallback === 'function') {
|
if (typeof requestIdleCallback === 'function') {
|
||||||
requestIdleCallback(function() { prefetchImage(guid, type); });
|
requestIdleCallback(function() { prefetchImage(guid, type, el.dataset.imageUrl); });
|
||||||
} else {
|
} else {
|
||||||
setTimeout(function() { prefetchImage(guid, type); }, 200);
|
setTimeout(function() { prefetchImage(guid, type, el.dataset.imageUrl); }, 200);
|
||||||
}
|
}
|
||||||
observer.unobserve(el);
|
observer.unobserve(el);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')));
|
$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
|
@endphp
|
||||||
@if($hasCover)
|
@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'] }}"
|
alt="{{ $anime['title'] }}"
|
||||||
class="max-w-full h-auto mx-auto rounded shadow-lg"
|
class="max-w-full h-auto mx-auto rounded shadow-lg"
|
||||||
style="max-height: 400px;">
|
style="max-height: 400px;">
|
||||||
|
|||||||
@@ -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')));
|
$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
|
@endphp
|
||||||
@if($hasCover)
|
@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 }}"
|
alt="{{ $anime->title }}"
|
||||||
class="h-16 w-12 object-cover rounded shadow"
|
class="h-16 w-12 object-cover rounded shadow"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
|
|||||||
@@ -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">
|
<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">
|
<a href="{{ $guid ? url('/details/' . $guid) : '#' }}" class="block relative">
|
||||||
@if(!empty($result->cover))
|
@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 }}"
|
alt="{{ $result->title }}"
|
||||||
class="w-full h-64 object-cover"
|
class="w-full h-64 object-cover"
|
||||||
data-fallback-src="{{ url('/images/no-cover.png') }}">
|
data-fallback-src="{{ url('/images/no-cover.png') }}">
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<button type="button"
|
<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"
|
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-guid="{{ $result->guid }}"
|
||||||
|
data-image-url="{{ getImageAssetUrl('preview', $result->guid . '_thumb') }}"
|
||||||
title="View preview image">
|
title="View preview image">
|
||||||
<i class="fas fa-image mr-1"></i> Preview
|
<i class="fas fa-image mr-1"></i> Preview
|
||||||
</button>
|
</button>
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
<button type="button"
|
<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"
|
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-guid="{{ $result->guid }}"
|
||||||
|
data-image-url="{{ getImageAssetUrl('sample', $result->guid . '_thumb') }}"
|
||||||
title="View sample image">
|
title="View sample image">
|
||||||
<i class="fas fa-images mr-1"></i> Sample
|
<i class="fas fa-images mr-1"></i> Sample
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
@php
|
@php
|
||||||
$hasPreviewImage = isset($release->haspreview) && $release->haspreview == 1;
|
$hasPreviewImage = isset($release->haspreview) && $release->haspreview == 1;
|
||||||
$hasSampleImage = isset($release->jpgstatus) && $release->jpgstatus == 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
|
@endphp
|
||||||
|
|
||||||
@if($hasPreviewImage || $hasSampleImage)
|
@if($hasPreviewImage || $hasSampleImage)
|
||||||
@@ -145,8 +151,8 @@
|
|||||||
@if($hasPreviewImage)
|
@if($hasPreviewImage)
|
||||||
<!-- Preview image -->
|
<!-- Preview image -->
|
||||||
<div>
|
<div>
|
||||||
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ url('/covers/preview/' . $release->guid . '_thumb.webp') }}" data-image-title="Preview Image">
|
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ $previewImageUrl }}" data-image-title="Preview Image">
|
||||||
<img src="{{ url('/covers/preview/' . $release->guid . '_thumb.webp') }}"
|
<img src="{{ $previewImageUrl }}"
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
class="detail-gallery-image w-full h-auto rounded-lg"
|
class="detail-gallery-image w-full h-auto rounded-lg"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
@@ -158,8 +164,8 @@
|
|||||||
@if($hasSampleImage)
|
@if($hasSampleImage)
|
||||||
<!-- Sample image -->
|
<!-- Sample image -->
|
||||||
<div>
|
<div>
|
||||||
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ url('/covers/sample/' . $release->guid . '_thumb.webp') }}" data-image-title="Sample Image">
|
<div class="block cursor-pointer image-modal-trigger" data-image-url="{{ $sampleImageUrl }}" data-image-title="Sample Image">
|
||||||
<img src="{{ url('/covers/sample/' . $release->guid . '_thumb.webp') }}"
|
<img src="{{ $sampleImageUrl }}"
|
||||||
alt="Sample"
|
alt="Sample"
|
||||||
class="detail-gallery-image w-full h-auto rounded-lg"
|
class="detail-gallery-image w-full h-auto rounded-lg"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
|
|||||||
@@ -172,6 +172,7 @@
|
|||||||
<button type="button"
|
<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"
|
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-guid="{{ $release->guid }}"
|
||||||
|
data-image-url="{{ getImageAssetUrl('preview', $release->guid . '_thumb') }}"
|
||||||
title="View preview image">
|
title="View preview image">
|
||||||
<i class="fas fa-image mr-1"></i> Preview
|
<i class="fas fa-image mr-1"></i> Preview
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<button type="button"
|
<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"
|
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-guid="{{ $release->guid }}"
|
||||||
|
data-image-url="{{ getImageAssetUrl('preview', $release->guid . '_thumb') }}"
|
||||||
title="View preview image">
|
title="View preview image">
|
||||||
<i class="fas fa-image mr-1"></i> Preview
|
<i class="fas fa-image mr-1"></i> Preview
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-4 mb-4">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
<img class="rounded-lg shadow-md w-24 h-auto"
|
<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') }}"
|
data-fallback-src="{{ url('/covers/movies/no-cover.jpg') }}"
|
||||||
alt="{{ e($movie['title'] ?? '') }}" />
|
alt="{{ e($movie['title'] ?? '') }}" />
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition">
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<img class="rounded-lg shadow-sm max-w-[120px]"
|
<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'] ?? '') }}"/>
|
alt="{{ e($movie['title'] ?? '') }}"/>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<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="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<img class="rounded-lg shadow-sm w-20 h-auto shrink-0"
|
<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'] ?? '') }}"/>
|
alt="{{ e($movie['title'] ?? '') }}"/>
|
||||||
<div class="min-w-0 flex-1">
|
<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">
|
<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">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<!-- Movie Poster -->
|
<!-- Movie Poster -->
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img class="rounded-lg movie-poster-shadow w-32 h-48 object-cover"
|
<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'] ?? '') }}"/>
|
alt="{{ e($movie['title'] ?? '') }}"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<div class="flex gap-4 mb-4">
|
<div class="flex gap-4 mb-4">
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img class="rounded-lg movie-poster-shadow w-24 h-36 object-cover"
|
<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'] ?? '') }}"/>
|
alt="{{ e($movie['title'] ?? '') }}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-4 mb-4">
|
<div class="flex items-center gap-4 mb-4">
|
||||||
<img class="rounded-lg shadow-md w-24 h-auto"
|
<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') }}"
|
data-fallback-src="{{ url('/covers/tvshows/no-cover.jpg') }}"
|
||||||
alt="{{ e($show['title'] ?? '') }}" />
|
alt="{{ e($show['title'] ?? '') }}" />
|
||||||
|
|
||||||
@@ -87,4 +87,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<div class="shrink-0 relative">
|
<div class="shrink-0 relative">
|
||||||
<a href="{{ route('series', ['id' => $show->id]) }}" class="block">
|
<a href="{{ route('series', ['id' => $show->id]) }}" class="block">
|
||||||
@if($show->image)
|
@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
|
@else
|
||||||
<div class="w-full md:w-64 h-96 bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
<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>
|
<i class="fas fa-tv text-gray-400 text-5xl"></i>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<div class="lg:col-span-1">
|
<div class="lg:col-span-1">
|
||||||
<img class="series-detail-poster w-full h-auto rounded-lg"
|
<img class="series-detail-poster w-full h-auto rounded-lg"
|
||||||
alt="{{ $seriestitles ?? '' }} Poster"
|
alt="{{ $seriestitles ?? '' }} Poster"
|
||||||
src="{{ url('/covers/tvshows/' . $show['id'] . '.webp') }}"/>
|
src="{{ getImageAssetUrl('tvshows', (string) $show['id'], url('/covers/tvshows/no-cover.jpg')) }}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">{{ $seriessummary }}</p>
|
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">{{ $seriessummary }}</p>
|
||||||
|
|||||||
@@ -8,6 +8,35 @@ use Tests\TestCase;
|
|||||||
|
|
||||||
class HelperCoverUrlTest extends 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
|
public function test_unzip_gzip_file_returns_uncompressed_contents(): void
|
||||||
{
|
{
|
||||||
$path = tempnam(sys_get_temp_dir(), 'nntmux-gzip-');
|
$path = tempnam(sys_get_temp_dir(), 'nntmux-gzip-');
|
||||||
@@ -35,4 +64,39 @@ class HelperCoverUrlTest extends TestCase
|
|||||||
|
|
||||||
$this->assertStringContainsString('assets/images/no-cover.png', $url);
|
$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]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user