improved Modal with zoom and pagination. also implementing thumbnail generation
This commit is contained in:
+1
-1
@@ -21,7 +21,7 @@ venv/
|
|||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
# Ignore DB files
|
# Ignore DB files
|
||||||
app.db
|
imagerepo/
|
||||||
|
|
||||||
# Ignore Docker related files
|
# Ignore Docker related files
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|||||||
+42
-6
@@ -30,11 +30,22 @@ def index():
|
|||||||
@main.route('/gallery')
|
@main.route('/gallery')
|
||||||
@login_required
|
@login_required
|
||||||
def gallery():
|
def gallery():
|
||||||
recent_images = 21
|
per_page = 35
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
images = ImageRecord.query.order_by(
|
images = ImageRecord.query.order_by(
|
||||||
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
||||||
).limit(recent_images).all()
|
).paginate(page=page, per_page=per_page)
|
||||||
return render_template('gallery.html', images=images)
|
|
||||||
|
return render_template(
|
||||||
|
'gallery.html',
|
||||||
|
images=images,
|
||||||
|
has_next=images.has_next,
|
||||||
|
next_page_url=url_for('main.gallery', page=images.next_num) if images.has_next else '',
|
||||||
|
has_prev=images.has_prev,
|
||||||
|
prev_page_url=url_for('main.gallery', page=images.prev_num) if images.has_prev else ''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route('/images/<path:filename>')
|
@main.route('/images/<path:filename>')
|
||||||
def serve_image(filename):
|
def serve_image(filename):
|
||||||
@@ -43,12 +54,21 @@ def serve_image(filename):
|
|||||||
@main.route('/artist/<artist_name>')
|
@main.route('/artist/<artist_name>')
|
||||||
@login_required
|
@login_required
|
||||||
def artist_gallery(artist_name):
|
def artist_gallery(artist_name):
|
||||||
per_page=49
|
per_page = 35
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
images = ImageRecord.query.filter_by(artist=artist_name).order_by(
|
images = ImageRecord.query.filter_by(artist=artist_name).order_by(
|
||||||
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
||||||
).paginate(page=page, per_page=per_page)
|
).paginate(page=page, per_page=per_page)
|
||||||
return render_template('artist_gallery.html', images=images, artist=artist_name)
|
|
||||||
|
return render_template(
|
||||||
|
'artist_gallery.html',
|
||||||
|
images=images,
|
||||||
|
artist=artist_name,
|
||||||
|
has_next=images.has_next,
|
||||||
|
next_page_url=url_for('main.artist_gallery', artist_name=artist_name, page=images.next_num) if images.has_next else '',
|
||||||
|
has_prev=images.has_prev,
|
||||||
|
prev_page_url=url_for('main.artist_gallery', artist_name=artist_name, page=images.prev_num) if images.has_prev else ''
|
||||||
|
)
|
||||||
|
|
||||||
@main.route('/artists')
|
@main.route('/artists')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -82,12 +102,28 @@ def settings():
|
|||||||
abort(403)
|
abort(403)
|
||||||
return render_template('settings.html')
|
return render_template('settings.html')
|
||||||
|
|
||||||
|
@main.route('/trigger-thumbnail-generation', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def trigger_thumbnail_generation():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("You do not have permission to perform this action.", "danger")
|
||||||
|
return redirect(url_for('main.settings'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/import/thumbnail.flag', 'w') as f:
|
||||||
|
f.write("trigger")
|
||||||
|
flash("Thumbnail regeneration has been triggered.", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Failed to trigger thumbnail regeneration: {e}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('main.settings'))
|
||||||
|
|
||||||
@main.route('/import-images')
|
@main.route('/import-images')
|
||||||
@login_required
|
@login_required
|
||||||
def trigger_image_import():
|
def trigger_image_import():
|
||||||
with open('/import/trigger.flag', 'w') as f:
|
with open('/import/trigger.flag', 'w') as f:
|
||||||
f.write('start')
|
f.write('start')
|
||||||
flash("Image import triggered.")
|
flash("Image import triggered.", "success")
|
||||||
return redirect(url_for('main.settings'))
|
return redirect(url_for('main.settings'))
|
||||||
|
|
||||||
@main.route('/reset-db', methods=['POST'])
|
@main.route('/reset-db', methods=['POST'])
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ImageRecord(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
filename = db.Column(db.String(255), nullable=False)
|
filename = db.Column(db.String(255), nullable=False)
|
||||||
filepath = db.Column(db.String(512), nullable=False)
|
filepath = db.Column(db.String(512), nullable=False)
|
||||||
|
thumb_path = db.Column(db.String(512))
|
||||||
hash = db.Column(db.String(64), unique=True, nullable=False) # SHA256
|
hash = db.Column(db.String(64), unique=True, nullable=False) # SHA256
|
||||||
perceptual_hash = db.Column(db.String(16), nullable=True) # hex of 64-bit hash
|
perceptual_hash = db.Column(db.String(16), nullable=True) # hex of 64-bit hash
|
||||||
artist = db.Column(db.String(255), nullable=True)
|
artist = db.Column(db.String(255), nullable=True)
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const modal = document.getElementById('imageModal');
|
||||||
|
const modalImage = document.getElementById('modalImage');
|
||||||
|
const modalPrev = document.getElementById('modalPrev');
|
||||||
|
const modalNext = document.getElementById('modalNext');
|
||||||
|
const modalClose = document.getElementById('modalClose');
|
||||||
|
const modalImageWrapper = document.querySelector('.modal-image-wrapper');
|
||||||
|
|
||||||
|
modalImage.setAttribute('draggable', 'false');
|
||||||
|
|
||||||
|
let images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||||
|
let currentIndex = -1;
|
||||||
|
let isZoomed = false;
|
||||||
|
let isDragging = false;
|
||||||
|
let startX, startY, scrollLeft, scrollTop;
|
||||||
|
let dragStartX = 0;
|
||||||
|
let dragStartY = 0;
|
||||||
|
let dragMoved = false;
|
||||||
|
|
||||||
|
const hasNextPageInput = document.getElementById('hasNextPage');
|
||||||
|
const nextPageUrlInput = document.getElementById('nextPageUrl');
|
||||||
|
const hasPrevPageInput = document.getElementById('hasPrevPage');
|
||||||
|
const prevPageUrlInput = document.getElementById('prevPageUrl');
|
||||||
|
|
||||||
|
function updateImage(index) {
|
||||||
|
const img = images[index];
|
||||||
|
modalImage.src = img.dataset.full;
|
||||||
|
currentIndex = index;
|
||||||
|
resetZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(index) {
|
||||||
|
modal.classList.add('active');
|
||||||
|
updateImage(index);
|
||||||
|
history.replaceState({ modalIndex: index }, '', window.location.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modal.classList.remove('active');
|
||||||
|
modalImage.src = '';
|
||||||
|
currentIndex = -1;
|
||||||
|
resetZoom();
|
||||||
|
history.replaceState({}, '', window.location.pathname + window.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNext() {
|
||||||
|
if (currentIndex < images.length - 1) {
|
||||||
|
updateImage(currentIndex + 1);
|
||||||
|
} else if (hasNextPageInput?.value === "true") {
|
||||||
|
fetch(nextPageUrlInput.value)
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const newGallery = doc.querySelector('.gallery-grid');
|
||||||
|
const newImages = doc.querySelectorAll('.img-clickable');
|
||||||
|
|
||||||
|
if (newGallery && newImages.length > 0) {
|
||||||
|
const gallery = document.querySelector('.gallery-grid');
|
||||||
|
gallery.innerHTML = newGallery.innerHTML;
|
||||||
|
|
||||||
|
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||||
|
images.forEach((img, i) => {
|
||||||
|
img.addEventListener('click', () => openModal(i));
|
||||||
|
});
|
||||||
|
|
||||||
|
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
|
||||||
|
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
|
||||||
|
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
|
||||||
|
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
|
||||||
|
|
||||||
|
hasNextPageInput.value = newHasNext;
|
||||||
|
nextPageUrlInput.value = newNextUrl;
|
||||||
|
hasPrevPageInput.value = newHasPrev;
|
||||||
|
prevPageUrlInput.value = newPrevUrl;
|
||||||
|
|
||||||
|
history.pushState({}, '', nextPageUrlInput.value);
|
||||||
|
openModal(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrev() {
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
updateImage(currentIndex - 1);
|
||||||
|
} else if (hasPrevPageInput?.value === "true") {
|
||||||
|
fetch(prevPageUrlInput.value)
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const newGallery = doc.querySelector('.gallery-grid');
|
||||||
|
const newImages = doc.querySelectorAll('.img-clickable');
|
||||||
|
|
||||||
|
if (newGallery && newImages.length > 0) {
|
||||||
|
const gallery = document.querySelector('.gallery-grid');
|
||||||
|
gallery.innerHTML = newGallery.innerHTML;
|
||||||
|
|
||||||
|
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||||
|
images.forEach((img, i) => {
|
||||||
|
img.addEventListener('click', () => openModal(i));
|
||||||
|
});
|
||||||
|
|
||||||
|
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
|
||||||
|
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
|
||||||
|
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
|
||||||
|
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
|
||||||
|
|
||||||
|
hasNextPageInput.value = newHasNext;
|
||||||
|
nextPageUrlInput.value = newNextUrl;
|
||||||
|
hasPrevPageInput.value = newHasPrev;
|
||||||
|
prevPageUrlInput.value = newPrevUrl;
|
||||||
|
|
||||||
|
history.pushState({}, '', prevPageUrlInput.value);
|
||||||
|
openModal(images.length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleZoom() {
|
||||||
|
isZoomed = !isZoomed;
|
||||||
|
modalImageWrapper.classList.toggle('zoomed', isZoomed);
|
||||||
|
modalImageWrapper.style.cursor = isZoomed ? 'grab' : '';
|
||||||
|
if (!isZoomed) {
|
||||||
|
modalImageWrapper.scrollLeft = 0;
|
||||||
|
modalImageWrapper.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
isZoomed = false;
|
||||||
|
modalImageWrapper.classList.remove('zoomed');
|
||||||
|
modalImageWrapper.scrollLeft = 0;
|
||||||
|
modalImageWrapper.scrollTop = 0;
|
||||||
|
modalImageWrapper.style.cursor = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
modalImageWrapper.addEventListener('mousedown', (e) => {
|
||||||
|
if (!isZoomed) return;
|
||||||
|
isDragging = true;
|
||||||
|
dragMoved = false;
|
||||||
|
modalImageWrapper.style.cursor = 'grabbing';
|
||||||
|
startX = e.pageX - modalImageWrapper.offsetLeft;
|
||||||
|
startY = e.pageY - modalImageWrapper.offsetTop;
|
||||||
|
scrollLeft = modalImageWrapper.scrollLeft;
|
||||||
|
scrollTop = modalImageWrapper.scrollTop;
|
||||||
|
dragStartX = e.pageX;
|
||||||
|
dragStartY = e.pageY;
|
||||||
|
});
|
||||||
|
|
||||||
|
modalImageWrapper.addEventListener('mouseleave', () => {
|
||||||
|
isDragging = false;
|
||||||
|
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
|
||||||
|
});
|
||||||
|
|
||||||
|
modalImageWrapper.addEventListener('mouseup', () => {
|
||||||
|
isDragging = false;
|
||||||
|
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
|
||||||
|
});
|
||||||
|
|
||||||
|
modalImageWrapper.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging || !isZoomed) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragMoved = true;
|
||||||
|
const x = e.pageX - modalImageWrapper.offsetLeft;
|
||||||
|
const y = e.pageY - modalImageWrapper.offsetTop;
|
||||||
|
const walkX = x - startX;
|
||||||
|
const walkY = y - startY;
|
||||||
|
modalImageWrapper.scrollLeft = scrollLeft - walkX;
|
||||||
|
modalImageWrapper.scrollTop = scrollTop - walkY;
|
||||||
|
});
|
||||||
|
|
||||||
|
modalImageWrapper.addEventListener('click', (e) => {
|
||||||
|
if (dragMoved) {
|
||||||
|
dragMoved = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
modalClose?.addEventListener('click', closeModal);
|
||||||
|
modalPrev?.addEventListener('click', showPrev);
|
||||||
|
modalNext?.addEventListener('click', showNext);
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (!modal.classList.contains('active')) return;
|
||||||
|
if (e.key === 'ArrowRight') showNext();
|
||||||
|
if (e.key === 'ArrowLeft') showPrev();
|
||||||
|
if (e.key === 'Escape') closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
images.forEach((img, index) => {
|
||||||
|
img.addEventListener('click', () => openModal(index));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
if (modal.classList.contains('active')) {
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+82
-38
@@ -43,22 +43,39 @@ body {
|
|||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flash {
|
.flash {
|
||||||
padding: 0.5rem 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin-bottom: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash.success {
|
.flash-info {
|
||||||
background-color: #d4edda;
|
background-color: #3182ce; /* blue */
|
||||||
color: #155724;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash.danger {
|
.flash-message, .flash-success {
|
||||||
background-color: #f8d7da;
|
background-color: #38a169; /* green */
|
||||||
color: #721c24;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-warning {
|
||||||
|
background-color: #dd6b20; /* orange */
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-danger,
|
||||||
|
.flash-error {
|
||||||
|
background-color: #e53e3e; /* red */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@@ -84,7 +101,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 450px;
|
height: 450px;
|
||||||
background-color: rgba(1,1,1,.1);
|
background-color: #1c1c1c;
|
||||||
box-shadow: 0 3px 4px rgba(0,0,0,0.7);
|
box-shadow: 0 3px 4px rgba(0,0,0,0.7);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -140,6 +157,11 @@ body {
|
|||||||
border-color: #007BFF;
|
border-color: #007BFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-ellipsis {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
/* MODAL STYLES */
|
/* MODAL STYLES */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -157,72 +179,94 @@ body {
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 95vw;
|
width: 100%;
|
||||||
|
max-width: 1600px;
|
||||||
max-height: 95vh;
|
max-height: 95vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Modal body */
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body img {
|
/* Image wrapper for tall image logic */
|
||||||
max-width: 90vw;
|
.modal-image-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image-wrapper.zoomed {
|
||||||
|
overflow: auto;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image-wrapper.zoomed img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-image-wrapper img {
|
||||||
|
max-width: 100%;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.6);
|
box-shadow: 0 0 10px rgba(0,0,0,0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-filename {
|
/* Adjust for very tall images */
|
||||||
color: #fff;
|
.modal-image-wrapper.too-tall {
|
||||||
background: rgba(0, 0, 0, 0.6);
|
overflow-y: auto;
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
text-align: center;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal arrows and close */
|
.modal-image-wrapper.too-tall img {
|
||||||
|
width: 90vw;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal buttons */
|
||||||
.modal-button {
|
.modal-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 10%;
|
||||||
transform: translateY(-50%);
|
bottom: 10%;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
width: 60px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 2rem;
|
font-size: 2.5rem;
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal:hover .modal-button {
|
.modal-content:hover .modal-button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-prev {
|
.modal-prev {
|
||||||
left: -60px;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-next {
|
.modal-next {
|
||||||
right: -60px;
|
right: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
transform: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Artist Styling */
|
/* Artist Styling */
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% if images.pages > 1 %}
|
||||||
|
<div class="pagination-container">
|
||||||
|
{% set total_pages = images.pages %}
|
||||||
|
{% set current_page = images.page %}
|
||||||
|
{% set window_size = 7 %}
|
||||||
|
{% set endpoint = request.endpoint %}
|
||||||
|
|
||||||
|
{% if total_pages <= window_size %}
|
||||||
|
{% set start_page = 1 %}
|
||||||
|
{% set end_page = total_pages %}
|
||||||
|
{% elif current_page <= 4 %}
|
||||||
|
{% set start_page = 1 %}
|
||||||
|
{% set end_page = window_size %}
|
||||||
|
{% elif current_page > total_pages - 4 %}
|
||||||
|
{% set start_page = total_pages - window_size + 1 %}
|
||||||
|
{% set end_page = total_pages %}
|
||||||
|
{% else %}
|
||||||
|
{% set start_page = current_page - 3 %}
|
||||||
|
{% set end_page = current_page + 3 %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# First and Prev #}
|
||||||
|
{% if images.has_prev %}
|
||||||
|
{% if current_page > 1 %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=1, **request.view_args) }}">« First</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=images.prev_num, **request.view_args) }}">← Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Left ellipsis #}
|
||||||
|
{% if start_page > 1 %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=1, **request.view_args) }}">1</a>
|
||||||
|
{% if start_page > 2 %}
|
||||||
|
<span class="pagination-ellipsis">…</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Page numbers #}
|
||||||
|
{% for p in range(start_page, end_page + 1) %}
|
||||||
|
<a class="pagination-link {% if p == current_page %}active{% endif %}" href="{{ url_for(endpoint, page=p, **request.view_args) }}">{{ p }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{# Right ellipsis #}
|
||||||
|
{% if end_page < total_pages %}
|
||||||
|
{% if end_page < total_pages - 1 %}
|
||||||
|
<span class="pagination-ellipsis">…</span>
|
||||||
|
{% endif %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=total_pages, **request.view_args) }}">{{ total_pages }}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Next and Last #}
|
||||||
|
{% if images.has_next %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=images.next_num, **request.view_args) }}">Next →</a>
|
||||||
|
{% if current_page < total_pages %}
|
||||||
|
<a class="pagination-link" href="{{ url_for(endpoint, page=total_pages, **request.view_args) }}">Last »</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,54 +1,29 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block title %}{{ artist }}'s Gallery{% endblock %}
|
{% block title %}{{ artist }}'s Gallery{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 style="text-align:center; margin-top: 1rem;">{{ artist }}</h1>
|
<h1 style="text-align:center; margin-top: 1rem;">{{ artist }}</h1>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
<div class="pagination-container">
|
{% include '_pagination.html' %}
|
||||||
{% if images.has_prev %}
|
|
||||||
<a class="pagination-link" href="{{ url_for('main.artist_gallery', artist_name=artist, page=images.prev_num) }}">← Previous</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for p in range(1, images.pages + 1) %}
|
|
||||||
<a class="pagination-link {% if p == images.page %}active{% endif %}" href="{{ url_for('main.artist_gallery', artist_name=artist, page=p) }}">{{ p }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if images.has_next %}
|
|
||||||
<a class="pagination-link" href="{{ url_for('main.artist_gallery', artist_name=artist, page=images.next_num) }}">Next →</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gallery Grid -->
|
|
||||||
<div class="gallery-grid">
|
<div class="gallery-grid">
|
||||||
{% for image in images.items %}
|
{% for image in images %}
|
||||||
<div class="gallery-item">
|
<div class="gallery-item">
|
||||||
<div class="gallery-thumb img-clickable"
|
<div class="gallery-thumb img-clickable"
|
||||||
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}"
|
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '') ) }}"
|
||||||
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');"
|
style="background-image: url('{{ url_for('main.serve_image', filename=(image.thumb_path or image.filepath) | replace('/images/', '') ) }}');"
|
||||||
data-filename="{{ image.filename }}">
|
data-filename="{{ image.filename }}">
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
||||||
<span class="image-date">
|
<span class="image-date">
|
||||||
{{ (image.taken_at or image.imported_at) | datetimeformat }}
|
{{ image.taken_at or image.imported_at | datetimeformat }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
<div class="pagination-container">
|
{% include '_pagination.html' %}
|
||||||
{% if images.has_prev %}
|
|
||||||
<a class="pagination-link" href="{{ url_for('main.artist_gallery', artist_name=artist, page=images.prev_num) }}">← Previous</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for p in range(1, images.pages + 1) %}
|
|
||||||
<a class="pagination-link {% if p == images.page %}active{% endif %}" href="{{ url_for('main.artist_gallery', artist_name=artist, page=p) }}">{{ p }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if images.has_next %}
|
|
||||||
<a class="pagination-link" href="{{ url_for('main.artist_gallery', artist_name=artist, page=images.next_num) }}">Next →</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
{% block title %}Artists{% endblock %}
|
{% block title %}Artists{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 style="text-align:center; margin-top: 1rem;">Artists</h1>
|
<h1>Artists</h1>
|
||||||
|
|
||||||
<div class="artist-grid">
|
<div class="artist-grid">
|
||||||
{% for artist in artist_data %}
|
{% for artist in artist_data %}
|
||||||
<a href="{{ url_for('main.artist_gallery', artist_name=artist.name) }}" class="artist-card-link">
|
<a href="{{ url_for('main.artist_gallery', artist_name=artist.name) }}" class="artist-card-link">
|
||||||
@@ -12,7 +11,7 @@
|
|||||||
<div class="artist-preview">
|
<div class="artist-preview">
|
||||||
{% for image in artist.images %}
|
{% for image in artist.images %}
|
||||||
<div class="artist-thumb"
|
<div class="artist-thumb"
|
||||||
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');">
|
style="background-image: url('{{ url_for('main.serve_image', filename=(image.thumb_path or image.filepath) | replace('/images/', '') ) }}');">
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block title %}Gallery{% endblock %}
|
{% block title %}Gallery{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Gallery Thumbnail Grid -->
|
<h1 style="text-align:center; margin-top: 1rem;">Gallery</h1>
|
||||||
|
|
||||||
|
<!-- Pagination Controls (Top) -->
|
||||||
|
{% include '_pagination.html' %}
|
||||||
|
|
||||||
|
<!-- Gallery Grid -->
|
||||||
<div class="gallery-grid">
|
<div class="gallery-grid">
|
||||||
{% for image in images %}
|
{% for image in images.items %}
|
||||||
<div class="gallery-item">
|
<div class="gallery-item">
|
||||||
<div class="gallery-thumb img-clickable"
|
<div class="gallery-thumb img-clickable"
|
||||||
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}"
|
data-full="{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '') ) }}"
|
||||||
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '')) }}');"
|
style="background-image: url('{{ url_for('main.serve_image', filename=image.filepath | replace('/images/', '') ) }}');"
|
||||||
data-filename="{{ image.filename }}">
|
data-filename="{{ image.filename }}">
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
<a href="{{ url_for('main.artist_gallery', artist_name=image.artist) }}">{{ image.artist }}</a>
|
||||||
<span class="image-date">
|
<span class="image-date">
|
||||||
{{ (image.taken_at or image.imported_at) | datetimeformat }}
|
{{ image.taken_at or image.imported_at | datetimeformat }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls (Bottom) -->
|
||||||
|
{% include '_pagination.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+19
-75
@@ -31,15 +31,15 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- FLASH MESSAGES -->
|
<!-- FLASH MESSAGES -->
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
<div class="flash-container">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="flash {{ category }}">
|
<div class="flash flash-{{ category }}">{{ message }}</div>
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
@@ -49,86 +49,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if request.endpoint == 'main.gallery' or request.endpoint == 'main.artist_gallery' %}
|
||||||
|
<input type="hidden" id="hasNextPage" value="{{ has_next|lower }}">
|
||||||
|
<input type="hidden" id="nextPageUrl" value="{{ next_page_url }}">
|
||||||
|
<input type="hidden" id="hasPrevPage" value="{{ has_prev|lower }}">
|
||||||
|
<input type="hidden" id="prevPageUrl" value="{{ prev_page_url }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div id="imageModal" class="modal">
|
<div id="imageModal" class="modal">
|
||||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
<div class="modal-content">
|
||||||
<div class="modal-content bg-transparent border-0">
|
<!-- Navigation buttons -->
|
||||||
<button id="modalClose" class="modal-button modal-close">✕</button>
|
|
||||||
<button id="modalPrev" class="modal-button modal-prev">←</button>
|
<button id="modalPrev" class="modal-button modal-prev">←</button>
|
||||||
<button id="modalNext" class="modal-button modal-next">→</button>
|
<button id="modalNext" class="modal-button modal-next">→</button>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h5 id="modalFilename" class="modal-filename"></h5>
|
<div id="modalImageWrapper" class="modal-image-wrapper">
|
||||||
<img id="modalImage" src="" class="img-fluid">
|
<img id="modalImage" src="" alt="Full View">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/modal-pagination.js') }}"></script>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const modal = document.getElementById('imageModal');
|
|
||||||
const modalImage = document.getElementById('modalImage');
|
|
||||||
const modalClose = document.getElementById('modalClose');
|
|
||||||
const modalPrev = document.getElementById('modalPrev');
|
|
||||||
const modalNext = document.getElementById('modalNext');
|
|
||||||
const modalFilename = document.getElementById('modalFilename');
|
|
||||||
const images = Array.from(document.querySelectorAll('.img-clickable'));
|
|
||||||
|
|
||||||
let currentIndex = -1;
|
|
||||||
|
|
||||||
function openModal(index) {
|
|
||||||
currentIndex = index;
|
|
||||||
const img = images[currentIndex];
|
|
||||||
modalImage.src = img.dataset.full;
|
|
||||||
modalFilename.textContent = img.dataset.filename || '';
|
|
||||||
modal.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
modal.classList.remove('active');
|
|
||||||
modalImage.src = '';
|
|
||||||
modalFilename.textContent = '';
|
|
||||||
currentIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNext() {
|
|
||||||
if (images.length === 0) return;
|
|
||||||
currentIndex = (currentIndex + 1) % images.length;
|
|
||||||
const img = images[currentIndex];
|
|
||||||
modalImage.src = img.dataset.full;
|
|
||||||
modalFilename.textContent = img.dataset.filename || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPrev() {
|
|
||||||
if (images.length === 0) return;
|
|
||||||
currentIndex = (currentIndex - 1 + images.length) % images.length;
|
|
||||||
const img = images[currentIndex];
|
|
||||||
modalImage.src = img.dataset.full;
|
|
||||||
modalFilename.textContent = img.dataset.filename || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
images.forEach((img, index) => {
|
|
||||||
img.addEventListener('click', () => openModal(index));
|
|
||||||
});
|
|
||||||
|
|
||||||
modalClose.addEventListener('click', closeModal);
|
|
||||||
modalNext.addEventListener('click', showNext);
|
|
||||||
modalPrev.addEventListener('click', showPrev);
|
|
||||||
|
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal) closeModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (!modal.classList.contains('active')) return;
|
|
||||||
if (e.key === 'ArrowRight') showNext();
|
|
||||||
else if (e.key === 'ArrowLeft') showPrev();
|
|
||||||
else if (e.key === 'Escape') closeModal();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,9 +12,15 @@
|
|||||||
<button type="submit" class="btn primary-btn">Trigger Image Import</button>
|
<button type="submit" class="btn primary-btn">Trigger Image Import</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ url_for('main.trigger_thumbnail_generation') }}" method="post" onsubmit="return confirm('Are you sure you want to regenerate all thumbnails?');">
|
||||||
|
<button type="submit" class="btn warning-btn">Regenerate Thumbnails</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<form action="{{ url_for('main.reset_db') }}" method="post" onsubmit="return confirm('Are you sure you want to delete all image records? This action cannot be undone.');">
|
<form action="{{ url_for('main.reset_db') }}" method="post" onsubmit="return confirm('Are you sure you want to delete all image records? This action cannot be undone.');">
|
||||||
<button type="submit" class="btn danger-btn">Reset Image Database</button>
|
<button type="submit" class="btn danger-btn">Reset Image Database</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ from PIL import Image
|
|||||||
import exifread
|
import exifread
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import imagehash
|
import imagehash
|
||||||
|
import uuid
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import ImageRecord
|
from app.models import ImageRecord
|
||||||
|
|
||||||
|
THUMB_DIR = "/images/thumbs"
|
||||||
|
THUMB_SIZE = (400, 400)
|
||||||
|
|
||||||
def import_images_task(source_dir, dest_dir):
|
def import_images_task(source_dir, dest_dir):
|
||||||
imported = []
|
imported = []
|
||||||
|
os.makedirs(THUMB_DIR, exist_ok=True)
|
||||||
|
|
||||||
for artist_dir in os.listdir(source_dir):
|
for artist_dir in os.listdir(source_dir):
|
||||||
artist_path = os.path.join(source_dir, artist_dir)
|
artist_path = os.path.join(source_dir, artist_dir)
|
||||||
@@ -43,9 +48,19 @@ def import_images_task(source_dir, dest_dir):
|
|||||||
dest_path = os.path.join(target_dir, filename)
|
dest_path = os.path.join(target_dir, filename)
|
||||||
shutil.copy2(full_path, dest_path)
|
shutil.copy2(full_path, dest_path)
|
||||||
|
|
||||||
|
# Generate thumbnail
|
||||||
|
thumb_filename = f"{uuid.uuid4().hex}.jpg"
|
||||||
|
thumb_path = os.path.join(THUMB_DIR, thumb_filename)
|
||||||
|
try:
|
||||||
|
generate_thumbnail(full_path, thumb_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] Failed to generate thumbnail: {e}")
|
||||||
|
thumb_path = None
|
||||||
|
|
||||||
record = ImageRecord(
|
record = ImageRecord(
|
||||||
filename=filename,
|
filename=filename,
|
||||||
filepath=dest_path,
|
filepath=dest_path,
|
||||||
|
thumb_path=thumb_path,
|
||||||
hash=file_hash,
|
hash=file_hash,
|
||||||
perceptual_hash=str(phash) if phash else None,
|
perceptual_hash=str(phash) if phash else None,
|
||||||
artist=artist_dir,
|
artist=artist_dir,
|
||||||
@@ -61,11 +76,25 @@ def import_images_task(source_dir, dest_dir):
|
|||||||
imported.append(filename)
|
imported.append(filename)
|
||||||
|
|
||||||
# Add delay between each image
|
# Add delay between each image
|
||||||
|
db.session.commit()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return f"Imported {len(imported)} images."
|
return f"Imported {len(imported)} images."
|
||||||
|
|
||||||
|
def generate_thumbnail(image_path, size=(400, 400), overwrite=False):
|
||||||
|
thumbnail_dir = THUMB_DIR # update this accordingly
|
||||||
|
basename = os.path.basename(image_path)
|
||||||
|
thumb_path = os.path.join(thumbnail_dir, basename)
|
||||||
|
|
||||||
|
if not overwrite and os.path.exists(thumb_path):
|
||||||
|
return thumb_path
|
||||||
|
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
img.thumbnail(size)
|
||||||
|
img.save(thumb_path)
|
||||||
|
|
||||||
|
return thumb_path
|
||||||
|
|
||||||
def calculate_hash(file_path):
|
def calculate_hash(file_path):
|
||||||
hash_func = hashlib.sha256()
|
hash_func = hashlib.sha256()
|
||||||
@@ -74,7 +103,6 @@ def calculate_hash(file_path):
|
|||||||
hash_func.update(chunk)
|
hash_func.update(chunk)
|
||||||
return hash_func.hexdigest()
|
return hash_func.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def calculate_perceptual_hash(file_path):
|
def calculate_perceptual_hash(file_path):
|
||||||
try:
|
try:
|
||||||
with Image.open(file_path) as img:
|
with Image.open(file_path) as img:
|
||||||
@@ -83,7 +111,6 @@ def calculate_perceptual_hash(file_path):
|
|||||||
print(f"[WARN] Failed to compute perceptual hash for {file_path}: {e}")
|
print(f"[WARN] Failed to compute perceptual hash for {file_path}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_similar_image(phash, width, height, threshold=2):
|
def is_similar_image(phash, width, height, threshold=2):
|
||||||
for img in ImageRecord.query.filter(ImageRecord.perceptual_hash != None).all():
|
for img in ImageRecord.query.filter(ImageRecord.perceptual_hash != None).all():
|
||||||
try:
|
try:
|
||||||
@@ -96,7 +123,6 @@ def is_similar_image(phash, width, height, threshold=2):
|
|||||||
print(f"[WARN] Failed pHash comparison: {e}")
|
print(f"[WARN] Failed pHash comparison: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def extract_metadata(file_path):
|
def extract_metadata(file_path):
|
||||||
metadata = {
|
metadata = {
|
||||||
'file_size': None,
|
'file_size': None,
|
||||||
|
|||||||
+1
-2
@@ -7,5 +7,4 @@ services:
|
|||||||
- ./imagerepo/db/:/db # SQLite db storage
|
- ./imagerepo/db/:/db # SQLite db storage
|
||||||
- ./imagerepo/images:/images # where the app will store it's imported images
|
- ./imagerepo/images:/images # where the app will store it's imported images
|
||||||
- ./import:/import # where it will import images from
|
- ./import:/import # where it will import images from
|
||||||
|
- ./migrations:/app/migrations # for dev
|
||||||
|
|
||||||
+25
-3
@@ -1,9 +1,10 @@
|
|||||||
# image_import_loop.py
|
# image_import_worker.py
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
from app import create_app
|
from app import create_app
|
||||||
from app.utils.image_importer import import_images_task
|
from app.utils.image_importer import import_images_task
|
||||||
|
|
||||||
|
THUMBNAIL_FLAG = "/import/thumbnail.flag"
|
||||||
TRIGGER_FLAG = "/import/trigger.flag"
|
TRIGGER_FLAG = "/import/trigger.flag"
|
||||||
SOURCE_DIR = "/import"
|
SOURCE_DIR = "/import"
|
||||||
DEST_DIR = "/images"
|
DEST_DIR = "/images"
|
||||||
@@ -29,8 +30,29 @@ def monitor_and_import():
|
|||||||
print("[Image Importer] Trigger flag cleared.")
|
print("[Image Importer] Trigger flag cleared.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Image Importer] Failed to remove trigger flag: {e}")
|
print(f"[Image Importer] Failed to remove trigger flag: {e}")
|
||||||
else:
|
|
||||||
print("[Image Importer] No trigger flag. Sleeping...")
|
# Thumbnail generation Check
|
||||||
|
if os.path.exists(THUMBNAIL_FLAG):
|
||||||
|
print("[Image Importer] Thumbnail regeneration triggered.")
|
||||||
|
try:
|
||||||
|
from app.models import ImageRecord
|
||||||
|
from app.utils.image_importer import generate_thumbnail
|
||||||
|
|
||||||
|
for img in ImageRecord.query.all():
|
||||||
|
try:
|
||||||
|
generate_thumbnail(img.filepath, overwrite=True)
|
||||||
|
print(f"[Thumbnail] Regenerated for {img.filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Thumbnail] Failed for {img.filename}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Image Importer] Error during thumbnail regeneration: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(THUMBNAIL_FLAG)
|
||||||
|
print("[Image Importer] Thumbnail flag cleared.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Image Importer] Failed to remove thumbnail flag: {e}")
|
||||||
|
|
||||||
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""thumbnail processing
|
||||||
|
|
||||||
|
Revision ID: 861f9cae4fe5
|
||||||
|
Revises: 9c3559ba0427
|
||||||
|
Create Date: 2025-08-01 22:32:30.089380
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '861f9cae4fe5'
|
||||||
|
down_revision = '9c3559ba0427'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('image_record', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('thumb_path', sa.String(length=512), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('image_record', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('thumb_path')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user