diff --git a/.gitignore b/.gitignore index e304d8d..77914a2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ lib/ lib64/ local/ share/ +<<<<<<< HEAD lib64 uploads/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..407abb2 --- /dev/null +++ b/app.py @@ -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/') +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') diff --git a/config.py b/config.py new file mode 100644 index 0000000..b929a5b --- /dev/null +++ b/config.py @@ -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 diff --git a/models.py b/models.py new file mode 100644 index 0000000..58df87c --- /dev/null +++ b/models.py @@ -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) diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..ae11af1 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,58 @@ + + + + + + Admin Interface + + + +

Admin Interface

+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} +

Upload New Image

+
+ + +
+

Uploaded Images

+ + + + + + + + + {% for photo in photos %} + + + + + + + + {% endfor %} +
IDThumbnailFilenameDate TakenTechnical Info
{{ photo.id }}Thumbnail{{ photo.input_filename }}{{ photo.date_taken.strftime('%Y-%m-%d %H:%M:%S') }}{{ photo.focal_length }}mm f/{{ photo.aperture }} {{ photo.shutter_speed }}s ISO{{ photo.iso }}
+ + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..fca4dac --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,24 @@ + + + + + + Admin Login + + +

Admin Login

+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} +
+ + +
+ +