Admin endpoints work
This commit is contained in:
parent
cd75219985
commit
8dfe806c0e
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ lib/
|
||||
lib64/
|
||||
local/
|
||||
share/
|
||||
<<<<<<< HEAD
|
||||
lib64
|
||||
|
||||
uploads/
|
||||
|
209
app.py
Normal file
209
app.py
Normal file
@ -0,0 +1,209 @@
|
||||
from flask import Flask, request, jsonify, render_template, redirect, url_for, flash, session, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
from models import Session as DBSession, Photo
|
||||
from config import load_or_create_config
|
||||
import os
|
||||
from datetime import datetime
|
||||
from PIL import Image, ExifTags
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import random
|
||||
from colorthief import ColorThief
|
||||
import colorsys
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24)
|
||||
config = load_or_create_config()
|
||||
|
||||
UPLOAD_FOLDER = config['directories']['upload']
|
||||
THUMBNAIL_FOLDER = config['directories']['thumbnail']
|
||||
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
|
||||
THUMBNAIL_SIZES = [256, 512, 768, 1024, 1536, 2048]
|
||||
|
||||
# Create upload and thumbnail directories if they don't exist
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
os.makedirs(THUMBNAIL_FOLDER, exist_ok=True)
|
||||
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['THUMBNAIL_FOLDER'] = THUMBNAIL_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 80 * 1024 * 1024 # 80MB limit
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.start()
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def get_highlight_color(image_path):
|
||||
color_thief = ColorThief(image_path)
|
||||
palette = color_thief.get_palette(color_count=6, quality=1)
|
||||
|
||||
# Convert RGB to HSV and find the color with the highest saturation
|
||||
highlight_color = max(palette, key=lambda rgb: colorsys.rgb_to_hsv(*rgb)[1])
|
||||
|
||||
return '#{:02x}{:02x}{:02x}'.format(*highlight_color)
|
||||
|
||||
def generate_thumbnails(filename):
|
||||
original_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
thumb_dir = os.path.join(THUMBNAIL_FOLDER, os.path.splitext(filename)[0])
|
||||
os.makedirs(thumb_dir, exist_ok=True)
|
||||
|
||||
for size in THUMBNAIL_SIZES:
|
||||
thumb_path = os.path.join(thumb_dir, f"{size}_{filename}")
|
||||
if not os.path.exists(thumb_path):
|
||||
with Image.open(original_path) as img:
|
||||
# Extract EXIF data
|
||||
exif_data = None
|
||||
if "exif" in img.info:
|
||||
exif_data = img.info["exif"]
|
||||
|
||||
# Resize image
|
||||
img.thumbnail((size, size), Image.LANCZOS)
|
||||
|
||||
# Save image with EXIF data
|
||||
if exif_data:
|
||||
img.save(thumb_path, exif=exif_data, optimize=True, quality=85)
|
||||
else:
|
||||
img.save(thumb_path, optimize=True, quality=85)
|
||||
|
||||
def generate_all_thumbnails():
|
||||
for filename in os.listdir(UPLOAD_FOLDER):
|
||||
if allowed_file(filename):
|
||||
generate_thumbnails(filename)
|
||||
|
||||
scheduler.add_job(generate_all_thumbnails, 'interval', minutes=5)
|
||||
scheduler.add_job(generate_all_thumbnails, 'date', run_date=datetime.now()) # Run once at startup
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/images')
|
||||
def get_images():
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = 20
|
||||
db_session = DBSession()
|
||||
photos = db_session.query(Photo).order_by(Photo.date_taken.desc()).offset((page - 1) * per_page).limit(per_page).all()
|
||||
|
||||
images = []
|
||||
for photo in photos:
|
||||
factor = random.randint(2, 3)
|
||||
if photo.height < 4000 or photo.width < 4000:
|
||||
factor = 1
|
||||
if photo.orientation == 6 or photo.orientation == 8:
|
||||
width, height = photo.height, photo.width
|
||||
else:
|
||||
width, height = photo.width, photo.height
|
||||
images.append({
|
||||
'imgSrc': f'/static/thumbnails/{os.path.splitext(photo.input_filename)[0]}/1536_{photo.input_filename}',
|
||||
'width': width / factor,
|
||||
'height': height / factor,
|
||||
'caption': photo.input_filename,
|
||||
'date': photo.date_taken.strftime('%y %m %d'),
|
||||
'technicalInfo': f"{photo.focal_length}MM | F/{photo.aperture} | {photo.shutter_speed} | ISO{photo.iso}",
|
||||
'highlightColor': photo.highlight_color
|
||||
})
|
||||
|
||||
has_more = db_session.query(Photo).count() > page * per_page
|
||||
db_session.close()
|
||||
|
||||
return jsonify({'images': images, 'hasMore': has_more})
|
||||
|
||||
@app.route('/admin')
|
||||
def admin():
|
||||
if 'logged_in' not in session:
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
db_session = DBSession()
|
||||
photos = db_session.query(Photo).order_by(Photo.date_taken.desc()).all()
|
||||
db_session.close()
|
||||
|
||||
return render_template('admin.html', photos=photos)
|
||||
|
||||
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||
def admin_login():
|
||||
if request.method == 'POST':
|
||||
if request.form['password'] == config['admin']['password']:
|
||||
session['logged_in'] = True
|
||||
return redirect(url_for('admin'))
|
||||
else:
|
||||
flash('Invalid password')
|
||||
return render_template('admin_login.html')
|
||||
|
||||
@app.route('/admin/upload', methods=['POST'])
|
||||
def admin_upload():
|
||||
if 'logged_in' not in session:
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Generate thumbnails
|
||||
generate_thumbnails(filename)
|
||||
|
||||
# Extract EXIF data
|
||||
exif = None
|
||||
with Image.open(file_path) as img:
|
||||
width, height = img.size
|
||||
exif = {
|
||||
ExifTags.TAGS[k]: v
|
||||
for k, v in img._getexif().items()
|
||||
if k in ExifTags.TAGS
|
||||
}
|
||||
|
||||
# Get image dimensions
|
||||
with Image.open(file_path) as img:
|
||||
width, height = img.size
|
||||
|
||||
exposure_time = exif['ExposureTime']
|
||||
if isinstance(exposure_time, tuple):
|
||||
exposure_fraction = f"{exposure_time[0]}/{exposure_time[1]}"
|
||||
else:
|
||||
exposure_fraction = f"1/{int(1/float(exposure_time))}"
|
||||
|
||||
# Create database entry
|
||||
db_session = DBSession()
|
||||
new_photo = Photo(
|
||||
input_filename=filename,
|
||||
thumbnail_filename=f"{os.path.splitext(filename)[0]}/256_{filename}",
|
||||
focal_length=str(exif.get('FocalLengthIn35mmFilm', exif.get('FocalLength', ''))),
|
||||
aperture=str(exif.get('FNumber', '')),
|
||||
shutter_speed=exposure_fraction,
|
||||
date_taken=datetime.strptime(str(exif.get('DateTime', '1970:01:01 00:00:00')), '%Y:%m:%d %H:%M:%S'),
|
||||
iso=int(exif.get('ISOSpeedRatings', 0)),
|
||||
orientation=int(exif.get('Orientation', 1)),
|
||||
width=width,
|
||||
height=height,
|
||||
highlight_color=get_highlight_color(THUMBNAIL_FOLDER + f"{os.path.splitext(filename)[0]}/256_{filename}")
|
||||
)
|
||||
db_session.add(new_photo)
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
|
||||
flash('File uploaded successfully')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
flash('Invalid file type')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
@app.route('/admin/logout')
|
||||
def admin_logout():
|
||||
session.pop('logged_in', None)
|
||||
flash('You have been logged out')
|
||||
return redirect(url_for('admin_login'))
|
||||
|
||||
@app.route('/static/thumbnails/<path:filename>')
|
||||
def serve_thumbnail(filename):
|
||||
return send_from_directory(THUMBNAIL_FOLDER, filename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=5002, host='0.0.0.0')
|
25
config.py
Normal file
25
config.py
Normal file
@ -0,0 +1,25 @@
|
||||
import toml
|
||||
import os
|
||||
import secrets
|
||||
|
||||
CONFIG_FILE = 'config.toml'
|
||||
|
||||
def load_or_create_config():
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
admin_password = secrets.token_urlsafe(16)
|
||||
config = {
|
||||
'admin': {
|
||||
'password': admin_password
|
||||
},
|
||||
'directories': {
|
||||
'upload': 'uploads',
|
||||
'thumbnail': 'thumbnails'
|
||||
}
|
||||
}
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
toml.dump(config, f)
|
||||
print(f"Generated new config file with admin password: {admin_password}")
|
||||
else:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
config = toml.load(f)
|
||||
return config
|
25
models.py
Normal file
25
models.py
Normal file
@ -0,0 +1,25 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Float
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Photo(Base):
|
||||
__tablename__ = 'photos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
input_filename = Column(String, nullable=False)
|
||||
thumbnail_filename = Column(String, nullable=False)
|
||||
focal_length = Column(String)
|
||||
aperture = Column(String)
|
||||
shutter_speed = Column(String)
|
||||
date_taken = Column(DateTime)
|
||||
iso = Column(Integer)
|
||||
width = Column(Integer)
|
||||
height = Column(Integer)
|
||||
highlight_color = Column(String)
|
||||
orientation = Column(Integer)
|
||||
|
||||
engine = create_engine('sqlite:///photos.db')
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
58
templates/admin.html
Normal file
58
templates/admin.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Interface</title>
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Admin Interface</h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<h2>Upload New Image</h2>
|
||||
<form action="{{ url_for('admin_upload') }}" method="POST" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept=".jpg,.jpeg,.png,.gif" required>
|
||||
<input type="submit" value="Upload">
|
||||
</form>
|
||||
<h2>Uploaded Images</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Thumbnail</th>
|
||||
<th>Filename</th>
|
||||
<th>Date Taken</th>
|
||||
<th>Technical Info</th>
|
||||
</tr>
|
||||
{% for photo in photos %}
|
||||
<tr>
|
||||
<td>{{ photo.id }}</td>
|
||||
<td><img src="{{ url_for('static', filename='thumbnails/' + photo.thumbnail_filename) }}" alt="Thumbnail" width="100"></td>
|
||||
<td>{{ photo.input_filename }}</td>
|
||||
<td>{{ photo.date_taken.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>{{ photo.focal_length }}mm f/{{ photo.aperture }} {{ photo.shutter_speed }}s ISO{{ photo.iso }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
24
templates/admin_login.html
Normal file
24
templates/admin_login.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Admin Login</h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<input type="password" name="password" required>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user