thumbnails only for viewd images

This commit is contained in:
lelo 2025-04-21 15:39:38 +00:00
parent ba0bf2d731
commit c1ee80ce1d
5 changed files with 62 additions and 18 deletions

21
app.py
View File

@ -1,4 +1,4 @@
from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, abort from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, make_response, abort
import os import os
from PIL import Image, ImageOps from PIL import Image, ImageOps
import io import io
@ -286,10 +286,16 @@ def serve_file(subpath):
# Try to read the requested variant # Try to read the requested variant
with cache.read(cache_key) as reader: with cache.read(cache_key) as reader:
file_path = reader.name file_path = reader.name
cached_hit = True
except KeyError: except KeyError:
if small: # do not create when thumbnail requested
response = make_response('', 204)
return response
cached_hit = False
# On miss: generate both full-size and small thumb, then cache # On miss: generate both full-size and small thumb, then cache
with Image.open(full_path) as orig: with Image.open(full_path) as orig:
exif_bytes = orig.info.get('exif')
img = ImageOps.exif_transpose(orig) img = ImageOps.exif_transpose(orig)
variants = { variants = {
orig_key: (1920, 1920), orig_key: (1920, 1920),
@ -301,7 +307,7 @@ def serve_file(subpath):
if thumb.mode in ("RGBA", "P"): if thumb.mode in ("RGBA", "P"):
thumb = thumb.convert("RGB") thumb = thumb.convert("RGB")
bio = io.BytesIO() bio = io.BytesIO()
# dont pass exif_bytes here
thumb.save(bio, format='JPEG', quality=85) thumb.save(bio, format='JPEG', quality=85)
bio.seek(0) bio.seek(0)
cache.set(key, bio, read=True) cache.set(key, bio, read=True)
@ -320,7 +326,7 @@ def serve_file(subpath):
response.headers['Content-Disposition'] = 'inline' response.headers['Content-Disposition'] = 'inline'
response.headers['Cache-Control'] = 'public, max-age=86400' response.headers['Cache-Control'] = 'public, max-age=86400'
if do_log: if do_log and not small:
a.log_file_access( a.log_file_access(
cache_key, cache_key,
os.path.getsize(file_path), os.path.getsize(file_path),
@ -328,7 +334,7 @@ def serve_file(subpath):
ip_address, ip_address,
user_agent, user_agent,
session['device_id'], session['device_id'],
True cached_hit
) )
return response return response
@ -336,7 +342,9 @@ def serve_file(subpath):
try: try:
with cache.read(subpath) as reader: with cache.read(subpath) as reader:
file_path = reader.name file_path = reader.name
cached_hit = True
except KeyError: except KeyError:
cached_hit = False
try: try:
cache.set(subpath, open(full_path, 'rb'), read=True) cache.set(subpath, open(full_path, 'rb'), read=True)
with cache.read(subpath) as reader: with cache.read(subpath) as reader:
@ -347,8 +355,7 @@ def serve_file(subpath):
# 6) Build response for non-image # 6) Build response for non-image
filesize = os.path.getsize(file_path) filesize = os.path.getsize(file_path)
cached_hit = True
# Figure out download flag and filename # Figure out download flag and filename
ask_download = request.args.get('download') == 'true' ask_download = request.args.get('download') == 'true'
filename = os.path.basename(full_path) filename = os.path.basename(full_path)

View File

@ -40,7 +40,7 @@ services:
# Production-ready Gunicorn command with eventlet # Production-ready Gunicorn command with eventlet
command: > command: >
sh -c "pip install -r requirements.txt && sh -c "pip install -r requirements.txt &&
gunicorn --worker-class eventlet -w 1 -b 0.0.0.0:5000 app:app" gunicorn --worker-class eventlet -w 4 -b 0.0.0.0:5000 app:app"
networks: networks:

View File

@ -238,25 +238,54 @@ footer {
cursor: auto; cursor: auto;
} }
/* IMAGE GRID */
.images-grid { .images-grid {
display: grid; display: grid;
/* make each column exactly 150px wide */
grid-template-columns: repeat(auto-fill, 150px); grid-template-columns: repeat(auto-fill, 150px);
/* force every row exactly 150px tall */
grid-auto-rows: 150px; grid-auto-rows: 150px;
gap: 14px; gap: 14px;
} }
.image-item { .image-item {
/* ensure the thumbnail cant overflow its cell */ position: relative;
overflow: hidden; overflow: hidden;
border: 2px solid #ccc;
border-radius: 8px;
background: #f9f9f9;
} }
/* the filename overlay, centered at bottom */
.image-item .file-name {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 4px 0;
text-align: center;
font-size: 12px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
box-sizing: border-box;
transition: opacity 0.3s;
opacity: 1;
pointer-events: none;
}
/* once loaded, hide the filename overlay */
.image-item.loaded .file-name {
opacity: 0.5;
}
/* thumbnail styling unchanged */
.thumbnail { .thumbnail {
/* fill the full cell */
width: 100%; width: 100%;
height: 100%; height: 100%;
/* crop/scale to cover without stretching */
object-fit: cover; object-fit: cover;
border-radius: 4px; }
}
/* pop when loaded */
.image-item.loaded .thumbnail {
transform: scale(1.05);
}

View File

@ -58,9 +58,17 @@ function renderContent(data) {
const thumbUrl = `${file.path}?thumbnail=true`; const thumbUrl = `${file.path}?thumbnail=true`;
contentHTML += ` contentHTML += `
<div class="image-item"> <div class="image-item">
<a href="#" class="play-file image-link" data-url="${file.path}" data-file-type="${file.file_type}">
<img src="/media/${thumbUrl}" alt="${file.name}" class="thumbnail" /> <a href="#" class="play-file image-link"
data-url="${file.path}"
data-file-type="${file.file_type}">
<img
src="/media/${thumbUrl}"
class="thumbnail"
onload="this.closest('.image-item').classList.add('loaded')"
/>
</a> </a>
<span class="file-name">${file.name}</span>
</div> </div>
`; `;
}); });

View File

@ -35,7 +35,7 @@
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<a href="#"> <a href="/">
<img src="/custom_logo/logoW.png" alt="Logo" class="logo"> <img src="/custom_logo/logoW.png" alt="Logo" class="logo">
</a> </a>
<h1>{{ title_long }}</h1> <h1>{{ title_long }}</h1>