diff --git a/.gitignore b/.gitignore index 5d381cc..f48547f 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +db/ +images/ +cache/ diff --git a/2024-10-21T17:19:57,186481017+08:00.png b/2024-10-21T17:19:57,186481017+08:00.png deleted file mode 100644 index 225dd27..0000000 Binary files a/2024-10-21T17:19:57,186481017+08:00.png and /dev/null differ diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..dc16924 --- /dev/null +++ b/admin.py @@ -0,0 +1,107 @@ +import argparse +import json +import os +import secrets +from werkzeug.security import generate_password_hash +from duckduckgo_search import DDGS +import requests +import shutil + +def set_login_info(username, password): + salt = secrets.token_hex(16) + password_hash = generate_password_hash(password + salt) + key = secrets.token_hex(32) + + login_info = { + "login_info": { + "username": username, + "password_hash": password_hash, + "salt": salt + }, + "key": key + } + + os.makedirs('db', exist_ok=True) + with open('db/keys.json', 'w') as f: + json.dump(login_info, f, indent=4) + +def get_next_batch_number(): + existing_files = os.listdir('images') + batch_numbers = [int(f.split('_')[0]) for f in existing_files if '_' in f] + return max(batch_numbers, default=0) + 1 + +def download_images(query, total_images=100, batch_size=10): + os.makedirs('images', exist_ok=True) + ddgs = DDGS() + results = ddgs.images(query, max_results=total_images) + batch_number = get_next_batch_number() + downloaded_images = 0 + valid_extensions = {'jpg', 'jpeg', 'png', 'gif'} + + for i, result in enumerate(results): + image_url = result['image'] + try: + response = requests.get(image_url) + if response.status_code == 200: + ext = image_url.split('.')[-1].lower() + if ext not in valid_extensions: + ext = 'jpg' + filename = f'{batch_number:04d}_{(downloaded_images % batch_size) + 1:05d}.{ext}' + with open(f'images/{filename}', 'wb') as f: + f.write(response.content) + downloaded_images += 1 + else: + print(f"Failed to download image from {image_url}. Status code: {response.status_code}") + except requests.RequestException as e: + print(f"An error occurred while downloading image from {image_url}: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + if downloaded_images > 0 and downloaded_images % batch_size == 0: + batch_number += 1 + + print(f"Downloaded {downloaded_images} images for query '{query}'.") + +def clear_cache(): + cache_folder = 'cache' + if os.path.exists(cache_folder): + shutil.rmtree(cache_folder) + print(f"Cache directory '{cache_folder}' removed successfully.") + else: + print(f"Cache directory '{cache_folder}' does not exist.") + +def main(): + parser = argparse.ArgumentParser( + description='Set username and password for the application and optionally download images using DuckDuckGo.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + # Add an argument to enter user setting mode + parser.add_argument('--user-setting', action='store_true', help='Enter user setting mode to set username and password.') + + # Create a userset argument group + userset_group = parser.add_argument_group('userset', 'Set username and password') + userset_group.add_argument('-u', '--username', type=str, help='The username to set.') + userset_group.add_argument('-p', '--password', type=str, help='The password to set.') + + parser.add_argument('-q', '--query', type=str, help='The search query to download images for testing.') + parser.add_argument('-t', '--total_images', type=int, default=10, help='The total number of images to download.') + parser.add_argument('-b', '--batch_size', type=int, default=5, help='The number of images to download in each batch.') + parser.add_argument('-c', '--clear-cache', action='store_true', help='Remove the cache directory.') + + args = parser.parse_args() + + if args.user_setting: + if not args.username or not args.password: + parser.error('--user-setting requires --username and --password.') + set_login_info(args.username, args.password) + print(f"Username and password set successfully. Key generated and saved in db/keys.json.") + elif args.clear_cache: + clear_cache() + elif args.query: + download_images(args.query, args.total_images, args.batch_size) + else: + parser.print_help() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/app.py b/app.py index cd663d0..abfc228 100644 --- a/app.py +++ b/app.py @@ -1,31 +1,66 @@ -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session -from flask_socketio import SocketIO, emit +from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session, jsonify +from flask_socketio import SocketIO, emit, disconnect import os -from werkzeug.security import generate_password_hash, check_password_hash -from PIL import Image +import json +from werkzeug.security import check_password_hash +from PIL import Image, UnidentifiedImageError from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler app = Flask(__name__) -app.config['SECRET_KEY'] = 'your_secret_key' # Replace with a strong secret key + +# Load configuration from keys.json +with open('db/keys.json', 'r') as f: + config = json.load(f) + +app.config['SECRET_KEY'] = config['key'] socketio = SocketIO(app) app.config['UPLOAD_FOLDER'] = 'images' # Folder to store images app.config['CACHE_FOLDER'] = 'cache' # Folder to store cached resized images app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'} -# Fixed username and password -USERNAME = 'user' -PASSWORD = generate_password_hash('password') # Hashed password +USERNAME = config['login_info']['username'] +PASSWORD_HASH = config['login_info']['password_hash'] +SALT = config['login_info']['salt'] def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] +def is_image_valid(image_path): + try: + with Image.open(image_path) as img: + img.verify() # Verify that it is, in fact, an image + return True + except Exception: + return False + def resize_image(image_path, cache_path, size=(800, 450)): with Image.open(image_path) as img: img.thumbnail(size) - img.save(cache_path) + if img.mode in ("RGBA", "P"): # Convert to RGB if necessary + img = img.convert("RGB") + img.save(cache_path, format="JPEG") + +def create_cache(): + images = os.listdir(app.config['UPLOAD_FOLDER']) + cached_images = [] + + if not os.path.exists(app.config['CACHE_FOLDER']): + os.makedirs(app.config['CACHE_FOLDER']) + + for image in images: + image_path = os.path.join(app.config['UPLOAD_FOLDER'], image) + cache_path = os.path.join(app.config['CACHE_FOLDER'], image) + + if not os.path.exists(cache_path) and is_image_valid(image_path): + resize_image(image_path, cache_path) + + if is_image_valid(image_path): + cached_images.append(image) + + return cached_images @app.route('/') def index(): @@ -36,7 +71,7 @@ def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] - if username == USERNAME and check_password_hash(PASSWORD, password): + if username == USERNAME and check_password_hash(PASSWORD_HASH, password + SALT): session['logged_in'] = True return redirect(url_for('gallery')) else: @@ -54,43 +89,92 @@ def gallery(): if not session.get('logged_in'): return redirect(url_for('login')) - images = os.listdir(app.config['UPLOAD_FOLDER']) - cached_images = [] + cached_images = create_cache() + batches = [] + current_batch = [] + current_batch_number = None + unstructured_images = [] - if not os.path.exists(app.config['CACHE_FOLDER']): - os.makedirs(app.config['CACHE_FOLDER']) + for image in cached_images: + parts = image.split('_') + if len(parts) == 2 and parts[1].split('.')[0].isdigit(): + batch_number = int(parts[0]) + if current_batch_number is None: + current_batch_number = batch_number + if batch_number != current_batch_number: + batches.append(current_batch) + current_batch = [] + current_batch_number = batch_number + current_batch.append(image) + else: + unstructured_images.append(image) - for image in images: - image_path = os.path.join(app.config['UPLOAD_FOLDER'], image) - cache_path = os.path.join(app.config['CACHE_FOLDER'], image) + if current_batch: + batches.append(current_batch) + if unstructured_images: + batches.append(unstructured_images) - if not os.path.exists(cache_path): - resize_image(image_path, cache_path) - - cached_images.append(image) - - return render_template('gallery.html', images=cached_images) + return render_template('gallery.html', batches=batches) @app.route('/images/') def uploaded_file(filename): - return send_from_directory(app.config['CACHE_FOLDER'], filename) + if not session.get('logged_in'): + return redirect(url_for('login')) + + image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + if is_image_valid(image_path): + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + else: + return "Invalid image", 404 + +@app.route('/cached/') +def cached_file(filename): + if not session.get('logged_in'): + return redirect(url_for('login')) + + image_path = os.path.join(app.config['CACHE_FOLDER'], filename) + if is_image_valid(image_path): + return send_from_directory(app.config['CACHE_FOLDER'], filename) + else: + return "Invalid image", 404 + +@app.route('/api/images') +def api_images(): + if not session.get('logged_in'): + return jsonify([]) + + images = sorted( + os.listdir(app.config['UPLOAD_FOLDER']), + key=lambda x: os.path.getctime(os.path.join(app.config['UPLOAD_FOLDER'], x)), + reverse=True + ) + valid_images = [img for img in images if is_image_valid(os.path.join(app.config['UPLOAD_FOLDER'], img))] + return jsonify(valid_images) def emit_gallery_update(): socketio.emit('update_gallery') class Watcher(FileSystemEventHandler): def on_modified(self, event): - if not event.is_directory: + if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']): + create_cache() emit_gallery_update() def on_created(self, event): - if not event.is_directory: + if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']): + create_cache() emit_gallery_update() def on_deleted(self, event): - if not event.is_directory: + if not event.is_directory and event.src_path.startswith(app.config['UPLOAD_FOLDER']): + create_cache() emit_gallery_update() +@socketio.on('connect') +def handle_connect(): + if not session.get('logged_in'): + disconnect() + if __name__ == '__main__': observer = Observer() observer.schedule(Watcher(), path=app.config['UPLOAD_FOLDER'], recursive=False) diff --git a/cache/101260155_p0.png b/cache/101260155_p0.png deleted file mode 100644 index 85fae69..0000000 Binary files a/cache/101260155_p0.png and /dev/null differ diff --git a/cache/1b3aa1b096a23fec1def6fc275b1f4b4.png b/cache/1b3aa1b096a23fec1def6fc275b1f4b4.png deleted file mode 100644 index 95aa162..0000000 Binary files a/cache/1b3aa1b096a23fec1def6fc275b1f4b4.png and /dev/null differ diff --git a/cache/97144634_p0.png b/cache/97144634_p0.png index c029cb4..7b44c2a 100644 Binary files a/cache/97144634_p0.png and b/cache/97144634_p0.png differ diff --git a/cache/nextsec.png b/cache/nextsec.png index 7e17012..6db0406 100644 Binary files a/cache/nextsec.png and b/cache/nextsec.png differ diff --git a/cache/5月壁紙.png b/cache/5月壁紙.png index a06cb8a..05f28fb 100644 Binary files a/cache/5月壁紙.png and b/cache/5月壁紙.png differ diff --git a/images/101260155_p0.png b/images/101260155_p0.png deleted file mode 100644 index e0c8e40..0000000 Binary files a/images/101260155_p0.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fd7575e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask -Werkzeug \ No newline at end of file diff --git a/static/gallery_styles.css b/static/gallery_styles.css index 2a01cbf..50a5389 100644 --- a/static/gallery_styles.css +++ b/static/gallery_styles.css @@ -1,6 +1,7 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%); + background-attachment: fixed; margin: 0; padding: 0; display: flex; @@ -15,71 +16,62 @@ h1 { text-align: center; } .gallery-container { - display: grid; - gap: 10px; + display: flex; + flex-direction: column; + gap: 20px; padding: 20px; width: 100%; max-width: 1200px; box-sizing: border-box; + transition: all 0.5s ease-in-out; +} +.batch-container { + display: grid; + gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); +} +hr { + border: none; + border-top: 2px solid #ddd; + margin: 20px 0; +} +.responsive-img { + width: 100%; + height: auto; + aspect-ratio: 16 / 9; + object-fit: cover; + transition: opacity 0.5s ease-in-out, transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; +} +.responsive-img.loaded { + opacity: 1; +} +.responsive-img:not(.loaded) { + opacity: 0; +} +.responsive-img:hover { + transform: scale(1.05); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} +.added { + animation: fadeIn 0.5s ease-in-out; +} +.removed { + animation: fadeOut 0.5s ease-in-out; +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } } - /* Media queries for responsive padding */ @media (max-width: 1199px) { .gallery-container { padding-left: 40px; padding-right: 40px; } -} -@media (max-width: 991px) { - .gallery-container { - padding-left: 30px; - padding-right: 30px; - } -} -@media (max-width: 767px) { - .gallery-container { - padding-left: 20px; - padding-right: 20px; - } -} - -.responsive-img { - width: 100%; - height: auto; - border: 2px solid #ddd; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; - aspect-ratio: 16 / 9; - object-fit: cover; - opacity: 0; - transition: opacity 0.5s ease-in-out, transform 0.2s; -} -.responsive-img.loaded { - opacity: 1; -} -.responsive-img:hover { - transform: scale(1.05); -} - -/* Media queries for responsive columns */ -@media (min-width: 1200px) { - .gallery-container { - grid-template-columns: repeat(4, 1fr); - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .gallery-container { - grid-template-columns: repeat(3, 1fr); - } -} -@media (min-width: 768px) and (max-width: 991px) { - .gallery-container { - grid-template-columns: repeat(2, 1fr); - } -} -@media (max-width: 767px) { - .gallery-container { - grid-template-columns: 1fr; - } } \ No newline at end of file diff --git a/static/login_styles.css b/static/login_styles.css index 6ada16e..0f10a24 100644 --- a/static/login_styles.css +++ b/static/login_styles.css @@ -5,6 +5,8 @@ body { justify-content: center; align-items: center; height: 100vh; + background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%); + background-attachment: fixed; } form h1 { color: #333; diff --git a/static/styles.css b/static/styles.css index 16d10e2..77ec7c3 100644 --- a/static/styles.css +++ b/static/styles.css @@ -2,6 +2,7 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #ffeeee 25%, #dce2ff 75%); + background-attachment: fixed; margin: 0; padding: 0; display: flex; diff --git a/templates/gallery.html b/templates/gallery.html index 2608241..5756b2b 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -2,20 +2,140 @@ Image Gallery +

Image Gallery

diff --git a/templates/login.html b/templates/login.html index 0b36d68..7ddf8cf 100644 --- a/templates/login.html +++ b/templates/login.html @@ -2,6 +2,7 @@ Login +