Honor metadata in hardcore filters
This commit is contained in:
@@ -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
@@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user