"""Maintenance-queue Celery tasks. Currently holds the blocklist cleanup task that retroactively removes blocklisted tag names from the library. The earlier sync_character_fandoms task was removed on 2026-04-21 as part of the bare-name refactor. """ import logging from celery import shared_task from sqlalchemy import text from app import db from app.models import Tag log = logging.getLogger(__name__) @shared_task( name='app.tasks.maintenance.sweep_blocklisted_tag_from_images', soft_time_limit=60, time_limit=120, ) def sweep_blocklisted_tag_from_images(name: str) -> dict: """Remove a blocklisted tag name from every image that has it attached, then delete the Tag row itself. Scope is limited to kind='user' tags — that's the kind WD14's general category gets materialized as when accepted, which is the vast majority of blocklist hits. Character / fandom / artist / series / post / archive tags sharing the same name are left alone: those are deliberate, curated entities, and removing them silently because of a blocklist text match would be destructive. Returns a summary dict so the Celery result is introspectable. """ tag = Tag.query.filter_by(kind='user', name=name).first() if tag is None: log.info("sweep_blocklisted_tag_from_images: no kind='user' tag named %r; nothing to do", name) return {'name': name, 'tag_found': False, 'image_tags_deleted': 0, 'tag_deleted': False} result = db.session.execute( text("DELETE FROM image_tags WHERE tag_id = :tid"), {'tid': tag.id}, ) rowcount = result.rowcount or 0 db.session.delete(tag) db.session.commit() log.info( "sweep_blocklisted_tag_from_images: removed %r (tag_id=%d) from %d images", name, tag.id, rowcount, ) return { 'name': name, 'tag_found': True, 'image_tags_deleted': rowcount, 'tag_deleted': True, }