This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
imagerepo/app/main.py
T

249 lines
7.7 KiB
Python

# app/main.py
from flask import Blueprint, render_template, flash, redirect, url_for, abort, send_from_directory, request, jsonify
from flask_login import login_required, current_user
from sqlalchemy import desc, func, text
from sqlalchemy.orm import joinedload
from app.utils.image_importer import import_images_task
from app.models import ImageRecord, Tag, ArchiveRecord, image_tags
from app import db
import os
import shutil
main = Blueprint('main', __name__)
@main.route('/')
def index():
return redirect(url_for("main.gallery"))
# from random import choice # (kept from your original)
# image = ImageRecord.query.order_by(func.random()).first()
# background_url = None
# if image:
# path = image.thumb_path or image.filepath
# filename = path.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
def gallery():
"""
Gallery with pagination and optional tag filter (?tag=artist:NAME or any tag name).
Keeps your pagination shape so modal-pagination.js continues to work.
"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 35, type=int)
tag_name = request.args.get('tag')
# Base query + eager-load tags (cheap and future-proofs tag chips)
q = ImageRecord.query.options(joinedload(ImageRecord.tags))
# Optional tag filter
if tag_name:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
images = q.order_by(desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))) \
.paginate(page=page, per_page=per_page)
# Preserve next/prev URLs (carry tag= so modal pagination fetches stay filtered)
next_url = url_for('main.gallery', page=images.next_num, per_page=per_page, tag=tag_name) if images.has_next else ''
prev_url = url_for('main.gallery', page=images.prev_num, per_page=per_page, tag=tag_name) if images.has_prev else ''
return render_template(
'gallery.html',
images=images,
active_tag=tag_name,
has_next=images.has_next,
next_page_url=next_url,
has_prev=images.has_prev,
prev_page_url=prev_url
)
@main.route('/images/<path:filename>')
def serve_image(filename):
return send_from_directory('/images', filename)
@main.route('/tags')
# @login_required
def tag_list():
"""
Generic tag explorer:
/tags -> all tags
/tags?kind=user -> only user tags
/tags?kind=artist -> only artist tags
/tags?kind=archive -> only archive tags
Shows a few preview images per tag.
"""
images_per_tag = 3
kind = request.args.get('kind') # 'artist' | 'archive' | 'user' | None
q = Tag.query
if kind:
q = q.filter_by(kind=kind)
tags = q.order_by(Tag.name.asc()).all()
tag_data = []
for t in tags:
imgs = (
ImageRecord.query
.join(ImageRecord.tags)
.filter(Tag.id == t.id)
.order_by(desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at)))
.limit(images_per_tag)
.all()
)
# Display label: for artist/archive use the part after ':', else full name
label = t.name.split(":", 1)[1] if (t.kind in ("artist", "archive") and ":" in t.name) else t.name
tag_data.append({"name": label, "tag": t.name, "images": imgs, "kind": t.kind})
return render_template('tags_list.html',
tag_data=tag_data,
images_per_tag=images_per_tag,
active_kind=kind)
@main.route('/artists')
# @login_required
def artist_list():
# Backward-compatible route that now shows the Tag Explorer filtered to artist tags
return redirect(url_for('main.tag_list', kind='artist'))
@main.route('/artist/<artist_name>')
# @login_required
def artist_gallery(artist_name):
# Backward-compatible: go to gallery filtered by artist tag
return redirect(url_for('main.gallery', tag=f"artist:{artist_name}"))
@main.route('/settings')
# @login_required
def settings():
# if not current_user.is_admin:
# abort(403)
return render_template('settings.html')
@main.route('/trigger-thumbnail-generation', methods=['POST'])
# @login_required
def trigger_thumbnail_generation():
if not current_user.is_admin:
flash("You do not have permission to perform this action.", "danger")
return redirect(url_for('main.settings'))
try:
with open('/import/thumbnail.flag', 'w') as f:
f.write("trigger")
flash("Thumbnail regeneration has been triggered.", "success")
except Exception as e:
flash(f"Failed to trigger thumbnail regeneration: {e}", "danger")
return redirect(url_for('main.settings'))
@main.route('/import-images')
# @login_required
def trigger_image_import():
with open('/import/trigger.flag', 'w') as f:
f.write('start')
flash("Image import triggered.", "success")
return redirect(url_for('main.settings'))
@main.route('/reset-db', methods=['POST'])
# @login_required
def reset_db():
"""
Safe reset that works with Postgres (TRUNCATE ... CASCADE).
Falls back to ordered deletes if TRUNCATE isn't supported (e.g., SQLite).
"""
if not current_user.is_admin:
abort(403)
try:
# Postgres fast path
db.session.execute(text("""
TRUNCATE TABLE image_tags, archive_record, image_record, tag
RESTART IDENTITY CASCADE
"""))
db.session.commit()
except Exception:
# Fallback: manual delete order
db.session.execute(image_tags.delete())
ArchiveRecord.query.delete()
ImageRecord.query.delete()
Tag.query.delete()
db.session.commit()
# Optional: clear thumbs on disk to regenerate fresh
shutil.rmtree("/images/thumbs", ignore_errors=True)
os.makedirs("/images/thumbs", exist_ok=True)
flash("Database has been reset. All records removed.", "info")
return redirect(url_for('main.settings'))
# ----------------------------
# Tag add/remove endpoints
# ----------------------------
@main.get("/image/<int:image_id>/tags")
# @login_required
def list_tags(image_id):
from app.models import ImageRecord
img = ImageRecord.query.get_or_404(image_id)
return jsonify(ok=True, tags=[{"name": t.name, "kind": t.kind} for t in img.tags])
@main.post("/image/<int:image_id>/tags/add")
# @login_required
def add_tag(image_id):
"""
Minimal JSON endpoint to add a tag to an image.
Accepts either 'artist:NAME' / 'archive:...' or a bare freeform tag ('mytag').
"""
name = (request.form.get("name") or "").strip()
if not name:
abort(400)
# Normalize: keep explicit kind prefixes, otherwise treat as user tag
if ":" in name:
kind = name.split(":", 1)[0]
tag_name = name
else:
kind = "user"
tag_name = name
img = ImageRecord.query.get_or_404(image_id)
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
tag = Tag(name=tag_name, kind=kind)
db.session.add(tag)
db.session.flush()
if tag not in img.tags:
img.tags.append(tag)
db.session.commit()
return jsonify(ok=True, tag={"name": tag.name, "kind": tag.kind})
@main.post("/image/<int:image_id>/tags/remove")
# @login_required
def remove_tag(image_id):
"""
Minimal JSON endpoint to remove a tag from an image.
"""
name = (request.form.get("name") or "").strip()
img = ImageRecord.query.get_or_404(image_id)
tag = Tag.query.filter_by(name=name).first()
if tag and tag in img.tags:
img.tags.remove(tag)
db.session.commit()
return jsonify(ok=True)