thumbnails only for viewd images
This commit is contained in:
parent
ba0bf2d731
commit
c1ee80ce1d
21
app.py
21
app.py
@ -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()
|
||||||
# don’t 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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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 can’t 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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user