switched to tagging and added views to support it, in progress for other tagging functions.
This commit is contained in:
+156
-50
@@ -1,22 +1,23 @@
|
||||
# app/main.py
|
||||
|
||||
from flask import Blueprint, render_template, flash, redirect, url_for, abort, send_from_directory, request
|
||||
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
|
||||
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
|
||||
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():
|
||||
from random import choice
|
||||
from app.models import ImageRecord
|
||||
|
||||
from random import choice # (kept from your original)
|
||||
image = ImageRecord.query.order_by(func.random()).first()
|
||||
background_url = None
|
||||
|
||||
@@ -27,24 +28,40 @@ def index():
|
||||
|
||||
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))
|
||||
|
||||
images = ImageRecord.query.order_by(
|
||||
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
||||
).paginate(page=page, per_page=per_page)
|
||||
# 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=url_for('main.gallery', page=images.next_num, per_page=per_page) if images.has_next else '',
|
||||
next_page_url=next_url,
|
||||
has_prev=images.has_prev,
|
||||
prev_page_url=url_for('main.gallery', page=images.prev_num, per_page=per_page) if images.has_prev else ''
|
||||
prev_page_url=prev_url
|
||||
)
|
||||
|
||||
|
||||
@@ -52,51 +69,58 @@ def gallery():
|
||||
def serve_image(filename):
|
||||
return send_from_directory('/images', filename)
|
||||
|
||||
@main.route('/artist/<artist_name>')
|
||||
|
||||
@main.route('/tags')
|
||||
@login_required
|
||||
def artist_gallery(artist_name):
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 35, type=int)
|
||||
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
|
||||
|
||||
images = ImageRecord.query.filter_by(artist=artist_name).order_by(
|
||||
desc(func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at))
|
||||
).paginate(page=page, per_page=per_page)
|
||||
q = Tag.query
|
||||
if kind:
|
||||
q = q.filter_by(kind=kind)
|
||||
|
||||
return render_template(
|
||||
'artist_gallery.html',
|
||||
images=images,
|
||||
artist=artist_name,
|
||||
has_next=images.has_next,
|
||||
next_page_url=url_for('main.artist_gallery', artist_name=artist_name, page=images.next_num, per_page=per_page) if images.has_next else '',
|
||||
has_prev=images.has_prev,
|
||||
prev_page_url=url_for('main.artist_gallery', artist_name=artist_name, page=images.prev_num, per_page=per_page) if images.has_prev else ''
|
||||
)
|
||||
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():
|
||||
from app.models import ImageRecord
|
||||
from sqlalchemy import func
|
||||
# Backward-compatible route that now shows the Tag Explorer filtered to artist tags
|
||||
return redirect(url_for('main.tag_list', kind='artist'))
|
||||
|
||||
# Define how many images to show per artist card
|
||||
images_per_artist = 3 # Change this value to tweak
|
||||
@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}"))
|
||||
|
||||
# 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
|
||||
@@ -105,6 +129,7 @@ def settings():
|
||||
abort(403)
|
||||
return render_template('settings.html')
|
||||
|
||||
|
||||
@main.route('/trigger-thumbnail-generation', methods=['POST'])
|
||||
@login_required
|
||||
def trigger_thumbnail_generation():
|
||||
@@ -121,6 +146,7 @@ def trigger_thumbnail_generation():
|
||||
|
||||
return redirect(url_for('main.settings'))
|
||||
|
||||
|
||||
@main.route('/import-images')
|
||||
@login_required
|
||||
def trigger_image_import():
|
||||
@@ -129,13 +155,93 @@ def trigger_image_import():
|
||||
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)
|
||||
|
||||
ImageRecord.query.delete()
|
||||
db.session.commit()
|
||||
flash("Database has been reset. All image records removed.", "info")
|
||||
return redirect(url_for('main.settings'))
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user