added video import and playback functionality
This commit is contained in:
@@ -1,14 +1,12 @@
|
|||||||
// /app/static/js/modal-pagination.js
|
// /app/static/js/modal-pagination.js
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const modal = document.getElementById('imageModal');
|
const modal = document.getElementById('imageModal');
|
||||||
const modalImage = document.getElementById('modalImage');
|
|
||||||
const modalPrev = document.getElementById('modalPrev');
|
const modalPrev = document.getElementById('modalPrev');
|
||||||
const modalNext = document.getElementById('modalNext');
|
const modalNext = document.getElementById('modalNext');
|
||||||
const modalClose = document.getElementById('modalClose');
|
const modalClose = document.getElementById('modalClose');
|
||||||
const modalImageWrapper = document.querySelector('.modal-image-wrapper');
|
const modalImageWrapper = document.querySelector('.modal-image-wrapper');
|
||||||
|
|
||||||
modalImage.setAttribute('draggable', 'false');
|
|
||||||
|
|
||||||
let images = Array.from(document.querySelectorAll('.img-clickable'));
|
let images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||||
let currentIndex = -1;
|
let currentIndex = -1;
|
||||||
let isZoomed = false;
|
let isZoomed = false;
|
||||||
@@ -25,7 +23,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function updateImage(index) {
|
function updateImage(index) {
|
||||||
const img = images[index];
|
const img = images[index];
|
||||||
modalImage.src = img.dataset.full;
|
const fileType = img.dataset.type || "image";
|
||||||
|
modalImageWrapper.innerHTML = "";
|
||||||
|
|
||||||
|
if (fileType === "video") {
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = img.dataset.full;
|
||||||
|
video.controls = true;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.style.maxWidth = "100%";
|
||||||
|
video.style.maxHeight = "100%";
|
||||||
|
modalImageWrapper.appendChild(video);
|
||||||
|
} else {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.src = img.dataset.full;
|
||||||
|
image.alt = "Full View";
|
||||||
|
image.setAttribute("draggable", "false");
|
||||||
|
image.id = "modalImage";
|
||||||
|
modalImageWrapper.appendChild(image);
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
resetZoom();
|
resetZoom();
|
||||||
}
|
}
|
||||||
@@ -38,7 +55,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
modal.classList.remove('active');
|
modal.classList.remove('active');
|
||||||
modalImage.src = '';
|
modalImageWrapper.innerHTML = '';
|
||||||
currentIndex = -1;
|
currentIndex = -1;
|
||||||
resetZoom();
|
resetZoom();
|
||||||
history.replaceState({}, '', window.location.pathname + window.location.search);
|
history.replaceState({}, '', window.location.pathname + window.location.search);
|
||||||
@@ -65,15 +82,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
img.addEventListener('click', () => openModal(i));
|
img.addEventListener('click', () => openModal(i));
|
||||||
});
|
});
|
||||||
|
|
||||||
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
|
hasNextPageInput.value = doc.getElementById('hasNextPage')?.value || "false";
|
||||||
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
|
nextPageUrlInput.value = doc.getElementById('nextPageUrl')?.value || "";
|
||||||
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
|
hasPrevPageInput.value = doc.getElementById('hasPrevPage')?.value || "false";
|
||||||
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
|
prevPageUrlInput.value = doc.getElementById('prevPageUrl')?.value || "";
|
||||||
|
|
||||||
hasNextPageInput.value = newHasNext;
|
|
||||||
nextPageUrlInput.value = newNextUrl;
|
|
||||||
hasPrevPageInput.value = newHasPrev;
|
|
||||||
prevPageUrlInput.value = newPrevUrl;
|
|
||||||
|
|
||||||
history.pushState({}, '', nextPageUrlInput.value);
|
history.pushState({}, '', nextPageUrlInput.value);
|
||||||
openModal(0);
|
openModal(0);
|
||||||
@@ -103,15 +115,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
img.addEventListener('click', () => openModal(i));
|
img.addEventListener('click', () => openModal(i));
|
||||||
});
|
});
|
||||||
|
|
||||||
const newHasNext = doc.getElementById('hasNextPage')?.value || "false";
|
hasNextPageInput.value = doc.getElementById('hasNextPage')?.value || "false";
|
||||||
const newNextUrl = doc.getElementById('nextPageUrl')?.value || "";
|
nextPageUrlInput.value = doc.getElementById('nextPageUrl')?.value || "";
|
||||||
const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false";
|
hasPrevPageInput.value = doc.getElementById('hasPrevPage')?.value || "false";
|
||||||
const newPrevUrl = doc.getElementById('prevPageUrl')?.value || "";
|
prevPageUrlInput.value = doc.getElementById('prevPageUrl')?.value || "";
|
||||||
|
|
||||||
hasNextPageInput.value = newHasNext;
|
|
||||||
nextPageUrlInput.value = newNextUrl;
|
|
||||||
hasPrevPageInput.value = newHasPrev;
|
|
||||||
prevPageUrlInput.value = newPrevUrl;
|
|
||||||
|
|
||||||
history.pushState({}, '', prevPageUrlInput.value);
|
history.pushState({}, '', prevPageUrlInput.value);
|
||||||
openModal(images.length - 1);
|
openModal(images.length - 1);
|
||||||
@@ -121,6 +128,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleZoom() {
|
function toggleZoom() {
|
||||||
|
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||||||
|
if (!isImage) return;
|
||||||
isZoomed = !isZoomed;
|
isZoomed = !isZoomed;
|
||||||
modalImageWrapper.classList.toggle('zoomed', isZoomed);
|
modalImageWrapper.classList.toggle('zoomed', isZoomed);
|
||||||
modalImageWrapper.style.cursor = isZoomed ? 'grab' : '';
|
modalImageWrapper.style.cursor = isZoomed ? 'grab' : '';
|
||||||
@@ -139,7 +148,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modalImageWrapper.addEventListener('mousedown', (e) => {
|
modalImageWrapper.addEventListener('mousedown', (e) => {
|
||||||
if (!isZoomed) return;
|
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||||||
|
if (!isZoomed || !isImage) return;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragMoved = false;
|
dragMoved = false;
|
||||||
modalImageWrapper.style.cursor = 'grabbing';
|
modalImageWrapper.style.cursor = 'grabbing';
|
||||||
@@ -153,16 +163,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
modalImageWrapper.addEventListener('mouseleave', () => {
|
modalImageWrapper.addEventListener('mouseleave', () => {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
|
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||||||
|
if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab';
|
||||||
});
|
});
|
||||||
|
|
||||||
modalImageWrapper.addEventListener('mouseup', () => {
|
modalImageWrapper.addEventListener('mouseup', () => {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
if (isZoomed) modalImageWrapper.style.cursor = 'grab';
|
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||||||
|
if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab';
|
||||||
});
|
});
|
||||||
|
|
||||||
modalImageWrapper.addEventListener('mousemove', (e) => {
|
modalImageWrapper.addEventListener('mousemove', (e) => {
|
||||||
if (!isDragging || !isZoomed) return;
|
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||||||
|
if (!isDragging || !isZoomed || !isImage) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragMoved = true;
|
dragMoved = true;
|
||||||
const x = e.pageX - modalImageWrapper.offsetLeft;
|
const x = e.pageX - modalImageWrapper.offsetLeft;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gallery-thumb {
|
.gallery-thumb {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@@ -135,6 +136,18 @@ body {
|
|||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 3rem;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 8px rgba(0, 0, 0, 0.8);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
<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/', '') ) }}"
|
||||||
|
data-type="{{ 'video' if image.filename.lower().endswith(('.mp4', '.mov')) else 'image' }}"
|
||||||
style="background-image: url('{{ url_for('main.serve_image', filename=(image.thumb_path or 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 }}">
|
||||||
|
{% if image.filename.lower().endswith(('.mp4', '.mov')) %}
|
||||||
|
<div class="play-overlay">▶</div>
|
||||||
|
{% endif %}
|
||||||
</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">{{ image.taken_at or image.imported_at | datetimeformat }}</span>
|
<span class="image-date">{{ image.taken_at or image.imported_at | datetimeformat }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# app/utils/image_importer.py
|
# app/utils/image_importer.py
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os, shutil, hashlib, time
|
import os, shutil, hashlib, time
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -7,6 +6,7 @@ import exifread
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import imagehash
|
import imagehash
|
||||||
import uuid
|
import uuid
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import ImageRecord
|
from app.models import ImageRecord
|
||||||
@@ -29,7 +29,7 @@ def import_images_task(source_dir, dest_dir):
|
|||||||
|
|
||||||
for root, _, files in os.walk(artist_path):
|
for root, _, files in os.walk(artist_path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff')):
|
if file.lower().endswith(('.jpg', '.jpeg', '.jfif', '.png', '.gif', '.bmp', '.tiff', '.webp', '.mp4', '.mov')):
|
||||||
full_path = os.path.join(root, file)
|
full_path = os.path.join(root, file)
|
||||||
filename = os.path.basename(full_path)
|
filename = os.path.basename(full_path)
|
||||||
|
|
||||||
@@ -61,6 +61,11 @@ def import_images_task(source_dir, dest_dir):
|
|||||||
shutil.copy2(full_path, dest_path)
|
shutil.copy2(full_path, dest_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if dest_path.lower().endswith(('.mp4', '.mov')):
|
||||||
|
thumb_filename = f"{uuid.uuid4().hex}.jpg"
|
||||||
|
thumb_path = os.path.join("/images/thumbs", thumb_filename)
|
||||||
|
thumb_path = generate_video_thumbnail(dest_path, thumb_path)
|
||||||
|
else:
|
||||||
thumb_path = generate_thumbnail(dest_path)
|
thumb_path = generate_thumbnail(dest_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WARN] Failed to generate thumbnail for {filename}: {e}")
|
print(f"[WARN] Failed to generate thumbnail for {filename}: {e}")
|
||||||
@@ -128,6 +133,24 @@ def generate_thumbnail(image_path, size=(400, 400), overwrite=False):
|
|||||||
|
|
||||||
return str(thumb_path)
|
return str(thumb_path)
|
||||||
|
|
||||||
|
def generate_video_thumbnail(video_path, output_path, time_position="00:00:01"):
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
|
||||||
|
command = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-i", video_path,
|
||||||
|
"-ss", time_position,
|
||||||
|
"-vframes", "1",
|
||||||
|
"-vf", f"scale={THUMB_SIZE[0]}:-1",
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return output_path
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[WARN] Failed to extract video thumbnail for {video_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def calculate_hash(file_path):
|
def calculate_hash(file_path):
|
||||||
hash_func = hashlib.sha256()
|
hash_func = hashlib.sha256()
|
||||||
@@ -165,6 +188,29 @@ def extract_metadata(file_path):
|
|||||||
'taken_at': None
|
'taken_at': None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata['file_size'] = os.path.getsize(file_path)
|
||||||
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
|
|
||||||
|
if ext in ('.mp4', '.mov'):
|
||||||
|
# Handle video dimensions using ffprobe
|
||||||
|
try:
|
||||||
|
import subprocess, json
|
||||||
|
cmd = [
|
||||||
|
"ffprobe", "-v", "error",
|
||||||
|
"-select_streams", "v:0",
|
||||||
|
"-show_entries", "stream=width,height",
|
||||||
|
"-of", "json", file_path
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
stream = data.get("streams", [{}])[0]
|
||||||
|
metadata['width'] = stream.get('width')
|
||||||
|
metadata['height'] = stream.get('height')
|
||||||
|
metadata['format'] = 'mp4'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] Failed to extract video metadata: {file_path} – {e}")
|
||||||
|
return metadata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata['file_size'] = os.path.getsize(file_path)
|
metadata['file_size'] = os.path.getsize(file_path)
|
||||||
with Image.open(file_path) as img:
|
with Image.open(file_path) as img:
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Install PostgreSQL client
|
# Install PostgreSQL client
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y postgresql-client && \
|
apt-get install -y postgresql-client ffmpeg && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user