diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..79bc907 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Ignore test data +images/ +import/ + +# Ignore Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Ignore environment files +.env +.env.* + +# Ignore virtual environments +env/ +venv/ + +# Ignore Git files +.git +.gitignore + +# Ignore DB files +app.db + +# Ignore Docker related files +.dockerignore +docker-compose.yml \ No newline at end of file diff --git a/app/auth.py b/app/auth.py index cdbbf5f..7f2f1b4 100644 --- a/app/auth.py +++ b/app/auth.py @@ -45,7 +45,7 @@ def login(): user = User.query.filter_by(email=form.email.data).first() if user and check_password_hash(user.password_hash, form.password.data): login_user(user) - return redirect(url_for('main.index')) + return redirect(url_for('main.gallery')) else: flash('Login failed. Check username/password.', 'danger') return render_template('login.html', form=form) diff --git a/app/main.py b/app/main.py index b0b8f8b..e9af1cb 100644 --- a/app/main.py +++ b/app/main.py @@ -14,7 +14,18 @@ main = Blueprint('main', __name__) @main.route('/') def index(): - return render_template('index.html') + from random import choice + from app.models import ImageRecord + + image = ImageRecord.query.order_by(func.random()).first() + background_url = None + + if image: + filename = image.filepath.replace('/images/', '') + background_url = url_for('main.serve_image', filename=filename) + + return render_template('index.html', background_url=background_url) + @main.route('/gallery') @login_required @@ -25,11 +36,12 @@ def gallery(): ).limit(recent_images).all() return render_template('gallery.html', images=images) -@main.route('/static/images/') +@main.route('/images/') def serve_image(filename): return send_from_directory('/images', filename) @main.route('/artist/') +@login_required def artist_gallery(artist_name): per_page=49 page = request.args.get('page', 1, type=int) @@ -38,6 +50,30 @@ def artist_gallery(artist_name): ).paginate(page=page, per_page=per_page) return render_template('artist_gallery.html', images=images, artist=artist_name) +@main.route('/artists') +@login_required +def artist_list(): + from app.models import ImageRecord + from sqlalchemy import func + + # Define how many images to show per artist card + images_per_artist = 3 # Change this value to tweak + + # Get all unique artist names + artists = db.session.query(ImageRecord.artist).distinct().all() + artist_data = [] + + for (artist,) in artists: + images = ( + ImageRecord.query + .filter_by(artist=artist) + .order_by(ImageRecord.taken_at.desc().nullslast(), ImageRecord.imported_at.desc()) + .limit(images_per_artist) + .all() + ) + artist_data.append({'name': artist, 'images': images}) + + return render_template('artist_list.html', artist_data=artist_data, images_per_artist=images_per_artist) @main.route('/settings') @login_required @@ -49,13 +85,9 @@ def settings(): @main.route('/import-images') @login_required def trigger_image_import(): - if not current_user.is_admin: - abort(403) - source_dir = '/import' - dest_dir = '/images' - - result = import_images_task(source_dir, dest_dir) - flash(f"Image import completed: {result}") + with open('/import/trigger.flag', 'w') as f: + f.write('start') + flash("Image import triggered.") return redirect(url_for('main.settings')) @main.route('/reset-db', methods=['POST']) diff --git a/app/static/style.css b/app/static/style.css index bf5b61e..64792a6 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -223,4 +223,207 @@ body { transform: none; font-size: 1.5rem; opacity: 1; -} \ No newline at end of file +} + +/* Artist Styling */ +.artist-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1.5rem; + padding: 1rem; +} + +.artist-card { + background-color: #1c1c1c; + padding: 1rem; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); +} + +.artist-card h3 { + margin-bottom: 0.5rem; + text-align: center; + color: white; +} + +.artist-preview { + display: flex; + gap: 0; + justify-content: center; + overflow: hidden; + border-radius: 6px; +} + +.artist-thumb { + flex: 1; + height: 300px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + transition: transform 0.3s ease; +} + +.artist-thumb:hover { + transform: scale(1.05); +} + +.artist-card-link { + text-decoration: none; + color: inherit; + display: block; + transition: transform 0.2s ease; +} + +.artist-card-link:hover .artist-card { + transform: scale(1.02); + box-shadow: 0 4px 10px rgba(0,0,0,0.4); +} + +.settings-container { + max-width: 600px; + margin: 2rem auto; + padding: 1.5rem; + background: #f8f8f8; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.settings-section h2 { + margin-bottom: 1rem; + text-align: center; +} + +.settings-actions { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.btn { + padding: 0.75rem 1.5rem; + font-size: 1rem; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s ease; +} + +.primary-btn { + background-color: #0066cc; + color: white; +} + +.primary-btn:hover { + background-color: #004d99; +} + +.danger-btn { + background-color: #cc0033; + color: white; +} + +.danger-btn:hover { + background-color: #990024; +} + +.settings-info { + margin-top: 2rem; + font-size: 0.95rem; + color: #444; + text-align: center; +} + +.index-hero { + position: relative; + height: 100vh; + background-size: cover; + background-position: center; + filter: blur(0px); /* Required to initialize */ + overflow: hidden; +} + +.index-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(20, 20, 20, 0.5); + backdrop-filter: blur(16px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.index-message { + text-align: center; + color: #fff; + padding: 2rem; + max-width: 600px; +} + +.index-message h1 { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.index-message p { + font-size: 1.2rem; + margin-bottom: 2rem; +} + +.index-message .btn { + font-size: 1.1rem; +} + +/* Form Styling */ +.form-container { + max-width: 400px; + margin: 2rem auto; + padding: 2rem; + background-color: #ffffffdd; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.form-card { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; +} + +.form-label { + margin-bottom: 0.5rem; + font-weight: 600; +} + +.form-input { + padding: 0.6rem; + border: 1px solid #ccc; + border-radius: 5px; +} + +.form-button { + padding: 0.75rem; + background-color: #5a67d8; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.form-button:hover { + background-color: #434190; +} + +.form-footer-text { + margin-top: 1rem; + text-align: center; +} diff --git a/app/templates/artist_gallery.html b/app/templates/artist_gallery.html index 1373c1b..e06d680 100644 --- a/app/templates/artist_gallery.html +++ b/app/templates/artist_gallery.html @@ -3,7 +3,7 @@ {% block title %}{{ artist }}'s Gallery{% endblock %} {% block content %} -

{{ artist }}

+

{{ artist }}

@@ -21,23 +21,21 @@
-
diff --git a/app/templates/artist_list.html b/app/templates/artist_list.html new file mode 100644 index 0000000..31844ef --- /dev/null +++ b/app/templates/artist_list.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} +{% block title %}Artists{% endblock %} + +{% block content %} +

Artists

+ + +{% endblock %} diff --git a/app/templates/gallery.html b/app/templates/gallery.html index cd09f05..7c4a76b 100644 --- a/app/templates/gallery.html +++ b/app/templates/gallery.html @@ -8,18 +8,15 @@ {% for image in images %} {% endfor %}
- - {% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 0af6771..7f04072 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,8 +1,14 @@ {% extends "layout.html" %} - -{% block title %}Home{% endblock %} +{% block title %}Welcome{% endblock %} {% block content %} -

Welcome to the Flask Template App!

-

This is the index page.

+
+
+
+

Private Image Repository

+

This is a private image collection. You must register for access to browse and contribute.

+ Register +
+
+
{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index 63e7091..2584129 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -2,7 +2,7 @@ - Flask Template App + ImageRepo - {% block title %}{% endblock %} @@ -18,6 +18,7 @@ {% endif %} {% if current_user.is_authenticated %} + Artists Logout {% else %} Login diff --git a/app/templates/login.html b/app/templates/login.html index aeb5083..ddb9c2b 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -1,10 +1,30 @@ {% extends "layout.html" %} + +{% block title %}Login{% endblock %} + {% block content %} -

Login

-
+
+

Login

+ {{ form.hidden_tag() }} - {{ form.email(size=32, placeholder=form.email.label.text) }}
- {{ form.password(size=32, placeholder=form.password.label.text) }}
- {{ form.submit() }} - + +
+ {{ form.email.label(class="form-label") }} + {{ form.email(class="form-input", placeholder=form.email.label.text) }} +
+ +
+ {{ form.password.label(class="form-label") }} + {{ form.password(class="form-input", placeholder=form.password.label.text) }} +
+ +
+ {{ form.submit(class="form-button") }} +
+ + + +
{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html index 373e693..874039f 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -1,39 +1,52 @@ {% extends "layout.html" %} + +{% block title %}Register{% endblock %} + {% block content %} -

Register

-
+
+

Register

+ {{ form.hidden_tag() }} -

- {{ form.username(size=32, placeholder=form.username.label.text) }} - {% for error in form.username.errors %} - {{ error }} - {% endfor %} -

+
+ {{ form.username.label(class="form-label") }} + {{ form.username(class="form-input", placeholder=form.username.label.text) }} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
-

- {{ form.email(size=32, placeholder=form.email.label.text) }} - {% for error in form.email.errors %} - {{ error }} - {% endfor %} -

+
+ {{ form.email.label(class="form-label") }} + {{ form.email(class="form-input", placeholder=form.email.label.text) }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
-

- {{ form.password(size=32, placeholder=form.password.label.text) }} - {% for error in form.password.errors %} - {{ error }} - {% endfor %} -

+
+ {{ form.password.label(class="form-label") }} + {{ form.password(class="form-input", placeholder=form.password.label.text) }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
-

- {{ form.confirm_password(size=32, placeholder=form.confirm_password.label.text) }} - {% for error in form.confirm_password.errors %} - {{ error }} - {% endfor %} -

+
+ {{ form.confirm_password.label(class="form-label") }} + {{ form.confirm_password(class="form-input", placeholder=form.confirm_password.label.text) }} + {% for error in form.confirm_password.errors %} + {{ error }} + {% endfor %} +
-

- {{ form.submit() }} -

- +
+ {{ form.submit(class="form-button") }} +
+ + + +
{% endblock %} diff --git a/app/templates/settings.html b/app/templates/settings.html index a419b98..c3f114b 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -1,25 +1,25 @@ {% extends "layout.html" %} - {% block title %}Settings{% endblock %} {% block content %} -
-

Image Management

-

Use the options below to manage image data.

+

Settings

- +
+
+

Admin Actions

+
+
+ +
-
-
- + +
+ +
+

Use these tools to manually trigger an import or reset the image database. Resetting the database does not delete the actual image files.

+
+
{% endblock %} diff --git a/app/utils/image_importer.py b/app/utils/image_importer.py index 65d6a75..7c209ca 100644 --- a/app/utils/image_importer.py +++ b/app/utils/image_importer.py @@ -1,7 +1,7 @@ # app/utils/image_importer.py from datetime import datetime -import os, shutil, hashlib +import os, shutil, hashlib, time from PIL import Image import exifread import mimetypes @@ -60,6 +60,9 @@ def import_images_task(source_dir, dest_dir): db.session.add(record) imported.append(filename) + # Add delay between each image + time.sleep(1) + db.session.commit() return f"Imported {len(imported)} images." diff --git a/config.py b/config.py index c880dbb..c1eabaf 100644 --- a/config.py +++ b/config.py @@ -5,26 +5,26 @@ class Config: DB_TYPE = os.environ.get('DB_TYPE', 'sqlite').lower() # 'sqlite' or 'postgresql' if DB_TYPE == 'postgresql': - DB_USER = os.environ.get('DB_USER', 'postgres') + DB_USER = os.environ.get('DB_USER', 'imagerepo') DB_PASS = os.environ.get('DB_PASS', 'postgres') - DB_HOST = os.environ.get('DB_HOST', 'localhost') + DB_HOST = os.environ.get('DB_HOST', 'postgres') DB_PORT = os.environ.get('DB_PORT', '5432') - DB_NAME = os.environ.get('DB_NAME', 'postgres') + DB_NAME = os.environ.get('DB_NAME', 'imagerepo') SQLALCHEMY_DATABASE_URI = ( f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}' ) elif DB_TYPE == 'sqlite': - BASEDIR = os.path.abspath(os.path.dirname(__file__)) + DB_DIR = '/db' + os.makedirs(DB_DIR, exist_ok=True) # only works if /db is writable DB_NAME = os.environ.get('DB_NAME', 'app.db') - SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASEDIR, DB_NAME)}' + SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(DB_DIR, DB_NAME)}' + + else: raise ValueError(f"Unsupported DB_TYPE '{DB_TYPE}'. Use 'sqlite' or 'postgresql'.") SQLALCHEMY_TRACK_MODIFICATIONS = False - CELERY_BROKER_URL = 'redis://localhost:6379/0' - CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' - diff --git a/docker-compose.yml b/docker-compose.yml index ad27e1d..1fb6d7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,8 @@ services: ports: - "5000:5000" volumes: - - ./:/app - - ./import:/import - - ./images:/images + - ./imagerepo/db/:/db # SQLite db storage + - ./imagerepo/images:/images # where the app will store it's imported images + - ./import:/import # where it will import images from + diff --git a/entrypoint.sh b/entrypoint.sh index 3adfd80..c9efc10 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -28,7 +28,7 @@ echo "Applying database migrations..." flask db upgrade # Start periodic image import in background -echo "Starting 2h import loop" +echo "import worker waiting for flag" python image_import_worker.py & # Start the Flask app diff --git a/image_import_worker.py b/image_import_worker.py index d6a91f0..424b6ef 100644 --- a/image_import_worker.py +++ b/image_import_worker.py @@ -1,20 +1,38 @@ # image_import_loop.py import time +import os from app import create_app from app.utils.image_importer import import_images_task +TRIGGER_FLAG = "/import/trigger.flag" +SOURCE_DIR = "/import" +DEST_DIR = "/images" +CHECK_INTERVAL = 10 # seconds + app = create_app() -def looped_import(source_dir='/import', dest_dir='/images', delay_seconds=7200): +def monitor_and_import(): with app.app_context(): - print("[Image Importer] Starting import task...") - try: - result = import_images_task(source_dir, dest_dir) - print(f"[Image Importer] {result}") - except Exception as e: - print(f"[Image Importer] Error: {e}") - print(f"[Image Importer] Sleeping for {delay_seconds / 3600:.1f} hours...\n") - time.sleep(delay_seconds) + print("[Image Importer] Monitoring for trigger flag...") + + while True: + if os.path.exists(TRIGGER_FLAG): + print("[Image Importer] Trigger flag found. Starting import...") + try: + result = import_images_task(SOURCE_DIR, DEST_DIR) + print(f"[Image Importer] {result}") + except Exception as e: + print(f"[Image Importer] Error during import: {e}") + finally: + try: + os.remove(TRIGGER_FLAG) + print("[Image Importer] Trigger flag cleared.") + except Exception as e: + print(f"[Image Importer] Failed to remove trigger flag: {e}") + else: + print("[Image Importer] No trigger flag. Sleeping...") + + time.sleep(CHECK_INTERVAL) if __name__ == '__main__': - looped_import() + monitor_and_import()