Lychee version7.7.2 Did you check the latest Lychee version?Yes, I did Which PHP version are you using?PHP 8.5 Detailed description of the problemClicking a thumbnail opens the photo view, but the image area stays black. No network request for the image is issued at all — only POST Metrics::photo (204) fires. The affected photos all lack a medium2x size variant. Setting medium_2x = 0 in the settings does not change the behaviour; the viewer still issues no request and does not fall back to medium. Expected behaviour: The viewer falls back to the next available size variant (medium, or original). Additional context type width height short_path type 2 (medium2x) is absent. Thumbnails render fine. Requesting the medium file directly in a browser tab works and returns the image immediately (HTTP/2 200, content-type: image/jpeg), so the file, its permissions and the web server are fine. Across my library: 1344 photos, 944 have medium2x, 1250 have medium. All photos missing medium2x show the same black viewer. Ruled out
Steps to reproduce the issue
Diagnostics [REQUIRED]Self-diagnosis:Info: Hash: e474d204bc2d2a9a—dc35773dd4ead756 Info:Lychee Version (release): 7.7.2 Docker: linuxserver.io System: Linux exec() Available: yes Config:version: 070702 Browser & System [REQUIRED]Lychee version: 7.7.2 (release), DB 7.7.2 Please confirm (incomplete submissions will not be addressed)
|
Replies: 5 comments 1 reply
|
Hi @ezek1el The 2x variants will need to be bigger than 2160 in width and in height 1920. Given this:
The medium2x should be created I agree if |
|
I dug into this against the 1.
|
| case | request issued? |
|---|---|
src="/x.jpg" srcset="/x.jpg 1920w, /x2.jpg 3840w" |
yes |
src="/x.jpg" srcset="" |
yes |
set src then srcset="" via JS (Vue's order) |
yes |
img.src = ''; img.srcset = '' |
no request |
Only the last row reproduces your symptom. Corroborating evidence from your own instance: the album grid uses the same empty-srcset pattern (PhotoListItem.vue:22, AlbumThumbImage.vue:81) and your thumbnails render fine.
So a missing medium2x alone cannot produce "no request at all". Something is making src itself empty.
3. The real bug: a photo with medium2x but no medium gets its original URL stripped
In PhotoBox.vue the only way to get an empty src is a ?? '' fallback firing:
:src="photoStore.photo.size_variants.medium?.url ?? ''" <!-- line 58, Medium mode -->
:src="photoStore.photo.size_variants.original?.url ?? ''" <!-- line 71, Original mode -->medium.url is never null server-side. original.url can be — via the downgrade flag:
// app/Http/Resources/Models/SizeVariantsResouce.php:33
$downgrade = $should_downgrade && !$photo->isVideo() && $size_variants?->hasMedium() === true;
// :50
$this->original = $original?->toDataResource($downgrade);and hasMedium() is satisfied by either variant:
// app/Models/Extensions/SizeVariants.php:280
public function hasMedium(): bool
{
return $this->medium !== null || $this->medium2x !== null;
}Chain: photo has medium2x but no medium → hasMedium() true → original URL stripped → the frontend's mode selector only looks at medium:
// PhotoState.ts:103
if (this.photo?.size_variants.medium !== null) {
return ImageViewMode.Medium;
}
return ImageViewMode.Original;→ falls to Original → src = original?.url ?? '' → src="" → no request → black. The backend authorised stripping the original because "a medium exists", but the only medium that exists is one the viewer never uses.
Your numbers point right at this: 1344 photos, 1250 with medium — so 94 photos have no medium at all. And you have grants_full_photo_access: 0, which is what makes should_downgrade true even for you as owner in smart albums and search (AlbumPolicy::canAccessFullPhoto(), app/Policies/AlbumPolicy.php:327).
I could not reproduce "zero requests" from the variant set you posted (medium present, medium2x absent) — with medium present the mode is Medium and src is a valid URL. So could you check one of the affected photos for whether medium is also missing? If the black ones are from that set of 94, this is your bug. Worth also confirming the network panel isn't filtered to XHR only — "only POST Metrics::photo fires" is what an XHR-filtered view looks like, and no GET Photo request is expected either, since PhotoState.load() reads the photo out of the already-loaded album list.
Suggested fixes
Three separate things, smallest first:
PhotoState.ts:111— degrade instead of blanking, so a lonemediumstill gets a srcset:if (medium === null) return ""; if (medium2x === null) return `${medium.url} ${medium.width}w`;
PhotoBox.vue:58— give the viewer a real fallback chain. The embed widget already does this correctly (EmbedWidget.vue:268):The main viewer has no chain at all — it only ever considersreturn getUrl(variants.medium2x) ?? getUrl(variants.medium) ?? getUrl(variants.small2x) ?? "";
mediumthenoriginal;medium2x,small2xandsmallare never candidates.- The actual black-screen fix — don't strip
original.urlon the strength of a variant the viewer can't display. Narrowest change is atSizeVariantsResouce.php:33(guard the downgrade on$medium !== null) rather than changinghasMedium(), which has other callers.
Separately, GenerateThumbs printing the same "Did not create" line for "disabled in config" and "source too small" is what sent this investigation down the wrong path to begin with — distinguishing those two would be a cheap win.
Minor, unrelated: the w-descriptor srcset at PhotoBox.vue:60 has no companion sizes attribute, so it defaults to 100vw and over-fetches the 2x variant on narrow layouts.
|
Thanks both — @gitsult4n, your analysis is what got me to the actual cause, even though it turned out to be a different bug than either of us assumed. Summary first, evidence below. The affected photos are not missing What actually happens
// PhotoState.ts
this.photo?.precomputed.is_livephoto === true
? this.photo?.size_variants.medium === null ? `livephoto-original` : `livephoto-medium`
: ...
<div id="livephoto"
data-live-photo=""
data-proactively-loads-video="true"
data-photo-src="https://…/uploads/medium/0a/b0/80ed02ea….jpg"
data-video-src="https://…/uploads/original/0a/b0/80ed02ea….mov"
style="width: 1920px; height: 933px;">
</div>Empty. Both data attributes are correct and both files return HTTP 200 when requested directly. Nothing ever fills the container: ['img','video','picture','source','canvas'].map(t => `${t}: ${document.querySelectorAll(t).length}`)
// ["img: 19", "video: 0", "picture: 0", "source: 0", "canvas: 0"]
// 19 = grid thumbnails and EXIF icons only — nothing in the viewer
typeof LivePhotosKit
// "undefined"That accounts for every symptom in my original report: no Not Android-specificThe empty container appears for any photo with The Android origin only explains one detail in my data: LivePhotosKit is not shipped, and cannot be addedThe build chunks reference It also cannot be supplied by the user, because the default CSP does not allow Apple's CDN. Injecting the script manually is blocked: Dropbox, Mollie, Stripe and PayPal are allow-listed; Counter-testUPDATE photos SET live_photo_short_path = NULL WHERE live_photo_short_path IS NOT NULL;All 171 affected photos render immediately and correctly via Library-wide: 1344 photos, 171 with On hypothesis 3 (
|
|
Checked both of your questions against the 1 No the player is not loaded anywhere and never wasThe only mention of the library in the whole tree is a comment explaining the ffmpeg container choice // app/ModelFunctions/MOVFormat.php:51
* But the JS package `livephotoskit/livephotoskit` which handles
* live photos on the frontend only supports Quicktime containersNo npm dependency in And the CSP allow list is a fixed four entries plus whatever the operator adds // config/secure-headers.php:709
'script-src' => [
'allow' => array_merge([
'https://www.dropbox.com/static/api/1/dropins.js',
'https://js.mollie.com',
'https://js.stripe.com',
'https://www.paypal.com/sdk/js',
], explode(',', (string) env('SECURITY_HEADER_SCRIPT_SRC_ALLOW', ''))),So this is not specific to your instance 2 The checksum is the wrong gateNothing on the render path reads // app/Http/Resources/Models/Utils/PreComputedPhotoData.php:34
$this->is_livephoto = $photo->live_photo_url !== null;
Gating on On the child imgRight direction but I would not lean on the library replacing it Nobody can currently load the player on a stock instance so what // PhotoState.ts imageViewMode
const has_player = typeof (window as any).LivePhotosKit !== "undefined";
if (this.photo?.precomputed.is_livephoto === true && has_player) {
return this.photo?.size_variants.medium === null
? ImageViewMode.LivePhotoOriginal
: ImageViewMode.LivePhotoMedium;
}Live photo markup only when a player exists otherwise the plain One more thing on the original variant <!-- PhotoBox.vue:92 -->
:data-photo-src="photoStore.photo.size_variants.original?.url"You said The real fixBundle the player through vite instead of pointing at Apple CDN Retitle looks right |
@ezek1el This should be fixed in the next release (hopefully)