Honor metadata in hardcore filters
This commit is contained in:
@@ -240,6 +240,29 @@ def _entry_text(item: Any) -> str:
|
||||
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]:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
positions = config.get("positions") or []
|
||||
if not positions:
|
||||
return True
|
||||
metadata_keys = _entry_position_keys(entry)
|
||||
if metadata_keys:
|
||||
return bool(set(metadata_keys) & set(positions))
|
||||
text = _entry_text(entry).lower()
|
||||
for position in positions:
|
||||
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
|
||||
@@ -648,12 +703,16 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
|
||||
selected = set(config.get("positions") or [])
|
||||
if not selected:
|
||||
return False
|
||||
text = _entry_text(entry).lower()
|
||||
matched = {
|
||||
position
|
||||
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
|
||||
if any(term in text for term in terms)
|
||||
}
|
||||
metadata_keys = _entry_position_keys(entry)
|
||||
if metadata_keys:
|
||||
matched = set(metadata_keys)
|
||||
else:
|
||||
text = _entry_text(entry).lower()
|
||||
matched = {
|
||||
position
|
||||
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
|
||||
if any(term in text for term in terms)
|
||||
}
|
||||
return bool(matched) and not bool(matched & selected)
|
||||
|
||||
|
||||
@@ -678,7 +737,7 @@ def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, An
|
||||
filtered = [
|
||||
value
|
||||
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 (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:
|
||||
text = _entry_text(template)
|
||||
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
|
||||
blocked = hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
|
||||
blocked = blocked or any(hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
|
||||
has_position_route = bool(fields & HARDCORE_POSITION_AXIS_KEYS) or bool(_entry_position_keys(template))
|
||||
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:
|
||||
filtered.append(template)
|
||||
return filtered or templates
|
||||
|
||||
+28
-5
@@ -3735,28 +3735,51 @@ def smoke_hardcore_position_config_policy() -> None:
|
||||
action_only,
|
||||
)
|
||||
_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(
|
||||
{
|
||||
"slug": "oral_sex",
|
||||
"item_templates": [
|
||||
{"template": "oral contact in {position}"},
|
||||
{"template": "metadata-specific oral contact", "position_key": "standing", "action_family": "oral"},
|
||||
{"template": "oral sex without a position axis"},
|
||||
{"template": "unsupported static template"},
|
||||
],
|
||||
"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"],
|
||||
},
|
||||
},
|
||||
base,
|
||||
)
|
||||
_expect(
|
||||
position_filtered["item_templates"] == [{"template": "oral contact in {position}"}],
|
||||
"Hardcore position policy did not filter templates by selected position requirements",
|
||||
position_filtered["item_templates"]
|
||||
== [
|
||||
{"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(
|
||||
position_filtered["item_axes"]["position"] == ["standing oral position"],
|
||||
"Hardcore position policy did not filter position axes by selected keys",
|
||||
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 or metadata",
|
||||
)
|
||||
filtered_categories = hardcore_position_config.filter_hardcore_categories_for_position(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user