diff --git a/automation/bin/kitchenowl_recipe_import.py b/automation/bin/kitchenowl_recipe_import.py index 03eca13..c4cc340 100755 --- a/automation/bin/kitchenowl_recipe_import.py +++ b/automation/bin/kitchenowl_recipe_import.py @@ -295,6 +295,10 @@ def enrich_recipe_node_from_dom(recipe_node: dict[str, Any], html: str) -> dict[ DURATION_RE = re.compile(r'^P(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\d+)S)?)?$') +TEXT_DURATION_PART_RE = re.compile( + r'(?P\d+(?:\.\d+)?)\s*(?Phours?|hrs?|hr|minutes?|mins?|min|seconds?|secs?|sec|s)\b', + re.IGNORECASE, +) def parse_duration_minutes(value: Any) -> int | None: @@ -314,6 +318,20 @@ def parse_duration_minutes(value: Any) -> int | None: seconds = int(m.group('seconds') or 0) total = hours * 60 + minutes + (1 if seconds >= 30 else 0) return total or None + textual_matches = list(TEXT_DURATION_PART_RE.finditer(value)) + if textual_matches: + total_minutes = 0.0 + for part in textual_matches: + amount = float(part.group('value')) + unit = part.group('unit').lower() + if unit.startswith(('hour', 'hr')): + total_minutes += amount * 60 + elif unit.startswith(('minute', 'min')): + total_minutes += amount + else: + total_minutes += amount / 60 + rounded = int(total_minutes + 0.5) + return rounded or None num = re.search(r'(\d+)', value) return int(num.group(1)) if num else None diff --git a/home/doris-kitchen/app/services/recipe_importer.py b/home/doris-kitchen/app/services/recipe_importer.py index 0a742bf..07c0f75 100644 --- a/home/doris-kitchen/app/services/recipe_importer.py +++ b/home/doris-kitchen/app/services/recipe_importer.py @@ -20,6 +20,10 @@ UNIT_WORDS = { STOPWORDS = {"fresh", "large", "small", "medium", "optional", "to", "taste", "for", "the", "a", "an"} TAG_KEYS = ("recipeCategory", "recipeCuisine", "keywords") DURATION_RE = re.compile(r"^P(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\d+)S)?)?$") +TEXT_DURATION_PART_RE = re.compile( + r"(?P\d+(?:\.\d+)?)\s*(?Phours?|hrs?|hr|minutes?|mins?|min|seconds?|secs?|sec|s)\b", + re.IGNORECASE, +) class ScriptExtractor(HTMLParser): @@ -410,6 +414,20 @@ def parse_duration_minutes(value: Any) -> int | None: seconds = int(match.group("seconds") or 0) total = hours * 60 + minutes + (1 if seconds >= 30 else 0) return total or None + textual_matches = list(TEXT_DURATION_PART_RE.finditer(text)) + if textual_matches: + total_minutes = 0.0 + for part in textual_matches: + amount = float(part.group("value")) + unit = part.group("unit").lower() + if unit.startswith(("hour", "hr")): + total_minutes += amount * 60 + elif unit.startswith(("minute", "min")): + total_minutes += amount + else: + total_minutes += amount / 60 + rounded = int(total_minutes + 0.5) + return rounded or None number = re.search(r"(\d+)", text) return int(number.group(1)) if number else None diff --git a/home/doris-kitchen/tests/test_tagging.py b/home/doris-kitchen/tests/test_tagging.py index ee1cee5..d18de37 100644 --- a/home/doris-kitchen/tests/test_tagging.py +++ b/home/doris-kitchen/tests/test_tagging.py @@ -6,10 +6,16 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from app.services.tagging import curate_recipe_tags -from app.services.recipe_importer import normalize_recipe_from_url +from app.services.recipe_importer import normalize_recipe_from_url, parse_duration_minutes class TaggingTests(unittest.TestCase): + def test_textual_durations_cover_weissman_style_time_strings(self): + self.assertEqual(parse_duration_minutes('1 hr, 15 min'), 75) + self.assertEqual(parse_duration_minutes('45 min + 12 hrs'), 765) + self.assertEqual(parse_duration_minutes('30 minutes'), 30) + self.assertEqual(parse_duration_minutes('90 sec'), 2) + def test_tater_tots_drop_boilerplate_and_gain_meaningful_tags(self): payload = normalize_recipe_from_url( 'https://www.joshuaweissman.com/recipes/the-best-tater-tots-ever-recipe' @@ -21,6 +27,9 @@ class TaggingTests(unittest.TestCase): self.assertIn('tater-tots', payload['tags']) self.assertIn('side-dish', payload['tags']) self.assertNotIn('beef', payload['tags']) + self.assertEqual(payload['prep_time'], 30) + self.assertEqual(payload['cook_time'], 45) + self.assertEqual(payload['time'], 75) def test_halal_chicken_rice_drops_boilerplate_and_infers_cuisine(self): payload = normalize_recipe_from_url( @@ -33,6 +42,9 @@ class TaggingTests(unittest.TestCase): self.assertIn('chicken', payload['tags']) self.assertIn('rice', payload['tags']) self.assertNotIn('skillet', payload['tags']) + self.assertEqual(payload['prep_time'], 30) + self.assertEqual(payload['cook_time'], 45) + self.assertEqual(payload['time'], 75) def test_existing_machine_tags_can_be_replaced_when_recipe_content_disagrees(self): tags = curate_recipe_tags(