"""Backend content types service.""" from __future__ import annotations from collections import defaultdict from datetime import UTC, datetime from sqlmodel import Session, select from models import Kink, KinkContentType, KinkTypeOverride from backend.constants import (DEFAULT_TYPE_OVERRIDES, DISCOVERABLE_CLUSTERS, DYNAMIC_CLUSTERS, EXACT_DYNAMIC_NAMES, EXACT_ROLE_NAMES, FETLIFE_DYNAMIC_KEYWORDS, META_CLUSTERS, PLAY_OVERRIDE_NAMES, ROLE_CLUSTERS, STARTER_CLUSTERS) def _seed_type_overrides(self) -> None: with Session(self.engine) as session: for normalized_name, cluster, content_kind, note in DEFAULT_TYPE_OVERRIDES: override_id = f"type_override_{cluster}_{self.slugify(normalized_name)}" if session.get(KinkTypeOverride, override_id): continue session.add( KinkTypeOverride( id=override_id, normalized_name=normalized_name, cluster=cluster, content_kind=content_kind, note=note, ) ) session.commit() def _derive_content_kind(self, name: str, cluster: str, override_map: dict[tuple[str, str], str] | None = None) -> tuple[str, str]: lowered_name = name.strip().lower() if override_map: if (lowered_name, cluster) in override_map: return override_map[(lowered_name, cluster)], "override:name+cluster" if (lowered_name, "*") in override_map: return override_map[(lowered_name, "*")], "override:name" if cluster in ROLE_CLUSTERS: return "role", "cluster:role" if cluster in DYNAMIC_CLUSTERS: return "dynamic", "cluster:dynamic" if cluster == "fetlife_fetish": if lowered_name in EXACT_ROLE_NAMES: return "dynamic", "fetlife:name:roleish_dynamic" if lowered_name in EXACT_DYNAMIC_NAMES: return "dynamic", "fetlife:name:dynamic" if any(keyword in lowered_name for keyword in FETLIFE_DYNAMIC_KEYWORDS): return "dynamic", "fetlife:name:dynamic_keyword" return "play", "cluster:fetlife_fetish" if cluster in META_CLUSTERS: return "meta", "cluster:meta" if lowered_name in PLAY_OVERRIDE_NAMES: return "play", "allowlist:name" if lowered_name in EXACT_ROLE_NAMES: return "role", "allowlist:role_name" if cluster in DISCOVERABLE_CLUSTERS or cluster in STARTER_CLUSTERS: return "play", "cluster:discoverable" return "meta", "fallback:meta" def _sync_content_types(self) -> dict[str, dict[str, str]]: with Session(self.engine) as session: overrides = session.exec(select(KinkTypeOverride)).all() override_map = { (row.normalized_name.strip().lower(), row.cluster.strip() or "*"): row.content_kind for row in overrides } kinks = session.exec(select(Kink)).all() rows_to_write = [] for kink in kinks: content_kind, evidence = self._derive_content_kind(kink.name, kink.cluster, override_map) rows_to_write.append((kink.id, content_kind, evidence, self._now_iso())) with self._sqlite() as conn: conn.executemany( """ INSERT INTO kinkcontenttype (kink_id, content_kind, evidence, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(kink_id) DO UPDATE SET content_kind = excluded.content_kind, evidence = excluded.evidence, updated_at = excluded.updated_at """, rows_to_write, ) conn.commit() stored = conn.execute( "SELECT kink_id, content_kind, evidence FROM kinkcontenttype" ).fetchall() return { row["kink_id"]: { "content_kind": row["content_kind"], "evidence": row["evidence"], } for row in stored } def _content_type_map(self, *, sync: bool = False) -> dict[str, dict[str, str]]: if sync: return self._sync_content_types() with Session(self.engine) as session: rows = session.exec(select(KinkContentType)).all() if rows: return { row.kink_id: { "content_kind": row.content_kind, "evidence": row.evidence, } for row in rows } return {} def _content_kind(self, item: dict[str, Any]) -> str: if item.get("content_kind"): return str(item["content_kind"]) cluster = item.get("cluster", "") lowered_name = str(item.get("name", "")).strip().lower() return self._derive_content_kind(lowered_name, cluster)[0]