Honor metadata in hardcore filters

This commit is contained in:
2026-06-27 14:19:47 +02:00
parent 7bc08ada47
commit ec79257613
2 changed files with 98 additions and 14 deletions
+64 -3
View File
@@ -240,6 +240,29 @@ def _entry_text(item: Any) -> str:
return str(item).strip() return str(item).strip()
def _metadata_tokens(item: Any, keys: tuple[str, ...]) -> set[str]:
if not isinstance(item, dict):
return set()
tokens: set[str] = set()
for key in keys:
for value in _list_from(item.get(key)):
token = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_")
if token and token != "any":
tokens.add(token)
return tokens
def _entry_position_keys(item: Any) -> list[str]:
if not isinstance(item, dict):
return []
values: list[Any] = []
if item.get("position_keys") is not None:
values.extend(_list_from(item.get("position_keys")))
if item.get("position_key") is not None:
values.append(item.get("position_key"))
return normalize_hardcore_position_values(values)
def hardcore_position_family_choices() -> list[str]: def hardcore_position_family_choices() -> list[str]:
return list(HARDCORE_POSITION_FAMILY_CHOICES) return list(HARDCORE_POSITION_FAMILY_CHOICES)
@@ -633,10 +656,42 @@ def hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str,
return False return False
def hardcore_entry_blocked_by_action(entry: Any, axis_name: str, config: dict[str, Any]) -> bool:
action_tokens = _metadata_tokens(entry, ("action_family", "action_type"))
family_tokens = _metadata_tokens(entry, ("position_family", "family"))
position_keys = set(_entry_position_keys(entry))
route_tokens = action_tokens | family_tokens
if not config.get("allow_toys", True) and action_tokens & {"toy", "toy_double"}:
return True
if not config.get("allow_double", True) and (action_tokens & {"double", "toy_double"} or "front_back" in position_keys):
return True
if not config.get("allow_anal", True) and "anal" in route_tokens:
return True
if not config.get("allow_oral", True) and "oral" in route_tokens:
return True
if not config.get("allow_outercourse", True) and "outercourse" in route_tokens:
return True
if not config.get("allow_penetration", True) and route_tokens & {"penetration", "penetrative", "toy_double", "anal"}:
return True
if not config.get("allow_foreplay", True) and "foreplay" in route_tokens:
return True
if not config.get("allow_interaction", True) and "interaction" in route_tokens:
return True
if not config.get("allow_manual", True) and "manual" in route_tokens:
return True
if not config.get("allow_climax", True) and "climax" in route_tokens:
return True
return hardcore_text_blocked_by_action(_entry_text(entry), axis_name, config)
def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool: def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
positions = config.get("positions") or [] positions = config.get("positions") or []
if not positions: if not positions:
return True return True
metadata_keys = _entry_position_keys(entry)
if metadata_keys:
return bool(set(metadata_keys) & set(positions))
text = _entry_text(entry).lower() text = _entry_text(entry).lower()
for position in positions: for position in positions:
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())): if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
@@ -648,6 +703,10 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
selected = set(config.get("positions") or []) selected = set(config.get("positions") or [])
if not selected: if not selected:
return False return False
metadata_keys = _entry_position_keys(entry)
if metadata_keys:
matched = set(metadata_keys)
else:
text = _entry_text(entry).lower() text = _entry_text(entry).lower()
matched = { matched = {
position position
@@ -678,7 +737,7 @@ def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, An
filtered = [ filtered = [
value value
for value in values for value in values
if not hardcore_text_blocked_by_action(_entry_text(value), axis_name, config) if not hardcore_entry_blocked_by_action(value, axis_name, config)
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and hardcore_position_entry_conflicts(value, config)) and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and hardcore_position_entry_conflicts(value, config))
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config)) and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
] ]
@@ -692,8 +751,10 @@ def filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> l
for template in templates: for template in templates:
text = _entry_text(template) text = _entry_text(template)
fields = {key for _, key, _, _ in Formatter().parse(text) if key} fields = {key for _, key, _, _ in Formatter().parse(text) if key}
blocked = hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS) has_position_route = bool(fields & HARDCORE_POSITION_AXIS_KEYS) or bool(_entry_position_keys(template))
blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields | {""}) blocked = hardcore_position_template_required(config) and not has_position_route
blocked = blocked or hardcore_entry_blocked_by_action(template, "", config)
blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields)
if not blocked: if not blocked:
filtered.append(template) filtered.append(template)
return filtered or templates return filtered or templates
+28 -5
View File
@@ -3735,28 +3735,51 @@ def smoke_hardcore_position_config_policy() -> None:
action_only, action_only,
) )
_expect(action_axis == ["boobjob body contact"], "Hardcore action filter policy did not block disabled oral/penetration text") _expect(action_axis == ["boobjob body contact"], "Hardcore action filter policy did not block disabled oral/penetration text")
action_axis_metadata = hardcore_position_config.filter_hardcore_axis(
"outer_act",
[
{"text": "generic contact route", "action_family": "outercourse", "position_family": "outercourse"},
{"text": "generic contact route", "action_family": "oral", "position_family": "oral"},
{"text": "generic contact route", "action_family": "penetration", "position_family": "penetrative"},
],
action_only,
)
_expect(
action_axis_metadata == [{"text": "generic contact route", "action_family": "outercourse", "position_family": "outercourse"}],
"Hardcore action filter policy did not honor structured action metadata",
)
position_filtered = hardcore_position_config.apply_hardcore_position_config_to_subcategory( position_filtered = hardcore_position_config.apply_hardcore_position_config_to_subcategory(
{ {
"slug": "oral_sex", "slug": "oral_sex",
"item_templates": [ "item_templates": [
{"template": "oral contact in {position}"}, {"template": "oral contact in {position}"},
{"template": "metadata-specific oral contact", "position_key": "standing", "action_family": "oral"},
{"template": "oral sex without a position axis"}, {"template": "oral sex without a position axis"},
{"template": "unsupported static template"}, {"template": "unsupported static template"},
], ],
"item_axes": { "item_axes": {
"position": ["standing oral position", "kneeling oral position"], "position": [
"standing oral position",
"kneeling oral position",
{"text": "generic standing pose", "position_key": "standing"},
{"text": "generic kneeling pose", "position_key": "kneeling"},
],
"oral_act": ["blowjob", "cunnilingus"], "oral_act": ["blowjob", "cunnilingus"],
}, },
}, },
base, base,
) )
_expect( _expect(
position_filtered["item_templates"] == [{"template": "oral contact in {position}"}], position_filtered["item_templates"]
"Hardcore position policy did not filter templates by selected position requirements", == [
{"template": "oral contact in {position}"},
{"template": "metadata-specific oral contact", "position_key": "standing", "action_family": "oral"},
],
"Hardcore position policy did not filter templates by selected position requirements or metadata",
) )
_expect( _expect(
position_filtered["item_axes"]["position"] == ["standing oral position"], position_filtered["item_axes"]["position"] == ["standing oral position", {"text": "generic standing pose", "position_key": "standing"}],
"Hardcore position policy did not filter position axes by selected keys", "Hardcore position policy did not filter position axes by selected keys or metadata",
) )
filtered_categories = hardcore_position_config.filter_hardcore_categories_for_position( filtered_categories = hardcore_position_config.filter_hardcore_categories_for_position(
[ [