diff --git a/app/main.py b/app/main.py index bdeb6dc..7a4ef9d 100644 --- a/app/main.py +++ b/app/main.py @@ -11,20 +11,11 @@ from app.models import ImageRecord, Tag, ArchiveRecord, image_tags, ImportTask, from app import db from app.utils.tag_names import normalize_display_name import os -import re import shutil main = Blueprint('main', __name__) -def _parse_character_fandom(name_after_prefix): - """Parse 'Name (Fandom)' -> ('Name', 'Fandom') or ('Name', None).""" - m = re.match(r'^(.+?)\s*\((.+)\)$', name_after_prefix) - if m: - return m.group(1).strip(), m.group(2).strip() - return name_after_prefix, None - - def _ensure_fandom_tag(fandom_name): """Find or create a fandom tag by bare name. Returns the Tag object. @@ -703,14 +694,24 @@ def search_tags(): @main.get("/image//tags") def list_tags(image_id): img = ImageRecord.query.get_or_404(image_id) - return jsonify(ok=True, tags=[ + tags_with_fandom = ( + db.session.query(Tag) + .join(image_tags, image_tags.c.tag_id == Tag.id) + .filter(image_tags.c.image_id == image_id) + .options(joinedload(Tag.fandom)) + .order_by(Tag.kind.asc(), Tag.name.asc()) + .all() + ) + return jsonify(tags=[ { + "id": t.id, "name": t.name, + "display_name": t.display_name, "kind": t.kind, "fandom_id": t.fandom_id if t.kind == "character" else None, - "fandom_name": (t.fandom.name.split(":", 1)[1] if t.fandom else None) if t.kind == "character" else None + "fandom_name": (t.fandom.name if t.fandom else None) if t.kind == "character" else None, } - for t in img.tags + for t in tags_with_fandom ]) @@ -1020,16 +1021,14 @@ def remove_tag(image_id): @main.get("/api/tag/") def get_tag(tag_id): """Get a single tag's details.""" - tag = Tag.query.get_or_404(tag_id) - # Get display name (without prefix) - display_name = tag.name.split(":", 1)[1] if ":" in tag.name else tag.name + tag = Tag.query.options(joinedload(Tag.fandom)).get_or_404(tag_id) result = { "id": tag.id, "name": tag.name, - "display_name": display_name, + "display_name": tag.display_name, "kind": tag.kind, "fandom_id": tag.fandom_id, - "fandom_name": tag.fandom.name.split(":", 1)[1] if tag.fandom else None + "fandom_name": tag.fandom.name if tag.fandom else None, } return jsonify(ok=True, tag=result) @@ -1167,13 +1166,12 @@ def get_post_metadata_by_name(tag_name): def update_tag(tag_id): """ Update a tag's name and/or kind. - For prefixed kinds (artist, archive, character, series, rating), - the name will be stored as 'kind:name'. - If the new name matches an existing tag, merge them: - - Move all images from source tag to target tag - - Delete the source tag - - Return the target tag info with merged=True + For character tags: rename = update Tag.name (bare). Fandom association + is NOT changed here — use /api/tag//set-fandom for that. + + If the new (name, kind [, fandom_id]) combination collides with an + existing tag, offer a merge (force_merge=true to commit). """ tag = Tag.query.get_or_404(tag_id) @@ -1184,22 +1182,28 @@ def update_tag(tag_id): if not new_name: return jsonify(ok=False, error="Name is required"), 400 - # Build the full tag name based on kind - prefixed_kinds = ("artist", "archive", "character", "series", "fandom", "rating") - if new_kind in prefixed_kinds: - # Remove any existing prefix from name if present - if ":" in new_name: - new_name = new_name.split(":", 1)[1] - full_name = f"{new_kind}:{new_name}" - else: - # User tags don't have prefixes - full_name = new_name + if new_kind in ("character", "fandom"): + new_name = normalize_display_name(new_name) + + # Check for duplicates using the new uniqueness shape + if new_kind == "character": + existing_q = Tag.query.filter( + Tag.kind == "character", + Tag.name == new_name, + Tag.fandom_id.is_(tag.fandom_id) if tag.fandom_id is None + else Tag.fandom_id == tag.fandom_id, + Tag.id != tag_id, + ) + else: + existing_q = Tag.query.filter( + Tag.kind == new_kind, + Tag.name == new_name, + Tag.id != tag_id, + ) + existing = existing_q.first() - # Check for duplicates (excluding current tag) - existing = Tag.query.filter(Tag.name == full_name, Tag.id != tag_id).first() if existing: if not force_merge: - # Ask user to confirm merge return jsonify( ok=False, error="A tag with this name already exists", @@ -1207,66 +1211,38 @@ def update_tag(tag_id): target_tag={ "id": existing.id, "name": existing.name, - "kind": existing.kind - } - ), 409 # Conflict status + "display_name": existing.display_name, + "kind": existing.kind, + }, + ), 409 # Merge: move all images from source tag to target tag - source_images = tag.images[:] # Copy list to avoid mutation during iteration + source_images = tag.images[:] merged_count = 0 for img in source_images: if existing not in img.tags: img.tags.append(existing) merged_count += 1 img.tags.remove(tag) - - # Delete the source tag db.session.delete(tag) db.session.commit() - return jsonify(ok=True, merged=True, merged_count=merged_count, tag={ "id": existing.id, "name": existing.name, - "kind": existing.kind + "display_name": existing.display_name, + "kind": existing.kind, }) - # Handle fandom-related updates - old_kind = tag.kind - old_name = tag.name - - # No conflict - just update the tag - tag.name = full_name + # Plain rename + tag.name = new_name tag.kind = new_kind - - # If editing a character tag, handle fandom from the name suffix - if new_kind == "character": - char_part = full_name.split(":", 1)[1] - char_name, fandom_name = _parse_character_fandom(char_part) - if fandom_name: - fandom_tag = _ensure_fandom_tag(fandom_name) - tag.fandom_id = fandom_tag.id - else: - tag.fandom_id = None - - # If renaming a fandom tag, cascade rename to all character tags that reference it - if new_kind == "fandom" and old_kind == "fandom" and full_name != old_name: - new_fandom_display = full_name.split(":", 1)[1] - old_fandom_display = old_name.split(":", 1)[1] if ":" in old_name else old_name - # Find all character tags linked to this fandom - linked_chars = Tag.query.filter_by(fandom_id=tag.id, kind="character").all() - for char_tag in linked_chars: - # Replace old fandom suffix with new one - char_part = char_tag.name.split(":", 1)[1] if ":" in char_tag.name else char_tag.name - char_base, _ = _parse_character_fandom(char_part) - char_tag.name = f"character:{char_base} ({new_fandom_display})" - db.session.commit() - return jsonify(ok=True, tag={ "id": tag.id, "name": tag.name, + "display_name": tag.display_name, "kind": tag.kind, - "fandom_id": tag.fandom_id + "fandom_id": tag.fandom_id, })