diff --git a/internal/integrations/mealie.go b/internal/integrations/mealie.go index dcee38f0..b9235d7c 100644 --- a/internal/integrations/mealie.go +++ b/internal/integrations/mealie.go @@ -28,73 +28,600 @@ type mealieRecipesResponse struct { Next *string `json:"next"` } -type mealieRecipeResponse struct { - ID string `json:"id"` - UserID string `json:"userId"` - GroupID string `json:"groupId"` - Name string `json:"name"` - Slug string `json:"slug"` - Image string `json:"image"` - RecipeYield string `json:"recipeYield"` - TotalTime string `json:"totalTime"` - PrepTime string `json:"prepTime"` - CookTime *string `json:"cookTime"` - PerformTime string `json:"performTime"` - Description string `json:"description"` - RecipeCategory []struct { +// MealieRecipe represents the structure of a Mealie JSON file. +type MealieRecipe struct { + ID string `json:"id"` + UserID string `json:"user_id"` + HouseholdID string `json:"household_id"` + GroupID string `json:"group_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Image string `json:"image"` + RecipeServings float64 `json:"recipe_servings"` + RecipeYieldQuantity float64 `json:"recipe_yield_quantity"` + RecipeYield string `json:"recipe_yield"` + TotalTime string `json:"total_time"` + PrepTime string `json:"prep_time"` + CookTime *string `json:"cook_time"` + PerformTime string `json:"perform_time"` + Description string `json:"description"` + RecipeCategory []recipeCategory `json:"recipe_category"` + Tags []struct { ID string `json:"id"` Name string `json:"name"` Slug string `json:"slug"` - } `json:"recipeCategory"` - Tags []struct { + } `json:"tags"` + Tools []models.HowToItem `json:"tools"` + Rating float64 `json:"rating"` + OrgURL string `json:"org_url"` + DateAdded string `json:"date_added"` + DateUpdated string `json:"date_updated"` + CreatedAt string `json:"created_at"` + UpdateAt string `json:"update_at"` + LastMade *string `json:"last_made"` + RecipeIngredient []recipeIngredient `json:"recipe_ingredient"` + RecipeInstructions []recipeInstruction `json:"recipe_instructions"` + Nutrition nutrition `json:"nutrition"` +} + +func (m *MealieRecipe) Schema() models.RecipeSchema { + category := "uncategorized" + if len(m.RecipeCategory) > 0 { + category = m.RecipeCategory[0].Name + } + + var yield int16 + if m.RecipeYieldQuantity > 0 { + v, _ := strconv.ParseInt(m.RecipeYield, 10, 16) + yield = int16(v) + } else if m.RecipeYieldQuantity > 0 { + yield = int16(m.RecipeYieldQuantity) + } else if m.RecipeServings > 0 { + yield = int16(m.RecipeServings) + } + + var cookTime string + if m.CookTime != nil { + cookTime = *m.CookTime + } + + dateUpdated := m.DateUpdated + if m.UpdateAt != "" { + dateUpdated = m.UpdateAt + } + + keywords := make([]string, 0, len(m.Tags)) + for _, tag := range m.Tags { + keywords = append(keywords, tag.Name) + } + + ingredients := make([]string, 0, len(m.RecipeIngredient)) + for _, ing := range m.RecipeIngredient { + ingredients = append(ingredients, ing.Display) + } + + instructions := make([]models.HowToItem, 0, len(m.RecipeInstructions)) + for _, ins := range m.RecipeInstructions { + instructions = append(instructions, models.NewHowToStep(ins.Text)) + } + + tools := make([]models.HowToItem, 0, len(m.Tools)) + for _, tool := range m.Tools { + tools = append(tools, models.NewHowToTool(tool.Text)) + } + + return models.RecipeSchema{ + AtContext: "https://schema.org", + AtType: &models.SchemaType{Value: "Recipe"}, + Category: &models.Category{Value: category}, + CookTime: cookTime, + DateCreated: m.DateAdded, + DateModified: dateUpdated, + DatePublished: m.DateAdded, + Description: &models.Description{Value: m.Description}, + Keywords: &models.Keywords{Values: strings.Join(keywords, ",")}, + Ingredients: &models.Ingredients{Values: ingredients}, + Instructions: &models.Instructions{Values: instructions}, + Name: m.Name, + NutritionSchema: m.Nutrition.Schema(), + PrepTime: m.PrepTime, + Tools: &models.Tools{Values: tools}, + TotalTime: m.TotalTime, + Yield: &models.Yield{Value: yield}, + URL: m.OrgURL, + } +} + +func (m *MealieRecipe) UnmarshalJSON(data []byte) error { + var temp struct { + ID string `json:"id"` + + UserID string `json:"user_id"` + UserIDOld string `json:"userId"` + + HouseholdID string `json:"household_id"` + + GroupID string `json:"group_id"` + GroupIDOld string `json:"groupId"` + + Name string `json:"name"` + Slug string `json:"slug"` + Image string `json:"image"` + + RecipeServings float64 `json:"recipe_servings"` + RecipeYieldQuantity float64 `json:"recipe_yield_quantity"` + + RecipeYield string `json:"recipe_yield"` + RecipeYieldOld string `json:"recipeYield"` + + TotalTime string `json:"total_time"` + TotalTimeOld string `json:"totalTime"` + + PrepTime string `json:"prep_time"` + PrepTimeOld string `json:"prepTime"` + + CookTime *string `json:"cook_time"` + CookTimeOld *string `json:"cookTime"` + + PerformTime string `json:"perform_time"` + PerformTimeOld string `json:"performTime"` + + Description string `json:"description"` + + RecipeCategory []recipeCategory `json:"recipe_category"` + RecipeCategoryOld []recipeCategory `json:"recipeCategory"` + + Tags []struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + } `json:"tags"` + Tools []models.HowToItem `json:"tools"` + Rating float64 `json:"rating"` + + OrgURL string `json:"org_url"` + OrgURLOld string `json:"orgURL"` + + DateAdded string `json:"date_added"` + DateAddedOld string `json:"dateAdded"` + + DateUpdated string `json:"date_updated"` + DateUpdatedOld string `json:"dateUpdated"` + + CreatedAt string `json:"created_at"` + CreatedAtOld string `json:"createdAt"` + + UpdateAt string `json:"update_at"` + UpdateAtOld string `json:"updateAt"` + + LastMade *string `json:"last_made"` + + RecipeIngredient []recipeIngredient `json:"recipe_ingredient"` + RecipeIngredientOld []recipeIngredient `json:"recipeIngredient"` + + RecipeInstructions []recipeInstruction `json:"recipe_instructions"` + RecipeInstructionsOld []recipeInstruction `json:"recipeInstructions"` + + Nutrition nutrition `json:"nutrition"` + } + + err := json.Unmarshal(data, &temp) + if err != nil { + return err + } + + m.ID = temp.ID + + if temp.UserID != "" { + m.UserID = temp.UserID + } else if temp.UserIDOld != "" { + m.UserID = temp.UserIDOld + } + + m.HouseholdID = temp.HouseholdID + + if temp.GroupID != "" { + m.GroupID = temp.GroupID + } else if temp.GroupIDOld != "" { + m.GroupID = temp.GroupIDOld + } + + m.Name = temp.Name + m.Slug = temp.Slug + m.Image = temp.Image + + m.RecipeServings = temp.RecipeServings + m.RecipeYieldQuantity = temp.RecipeYieldQuantity + + if temp.RecipeYield != "" { + m.RecipeYield = temp.RecipeYield + } else if temp.RecipeYieldOld != "" { + m.RecipeYield = temp.RecipeYieldOld + } else if int(temp.RecipeServings) > 0 { + m.RecipeYield = strconv.FormatFloat(temp.RecipeServings, 'f', -1, 64) + } else if int(temp.RecipeYieldQuantity) > 0 { + m.RecipeYield = strconv.FormatFloat(temp.RecipeYieldQuantity, 'f', -1, 64) + } + + if temp.TotalTime != "" { + m.TotalTime = temp.TotalTime + } else if temp.TotalTimeOld != "" { + m.TotalTime = temp.TotalTimeOld + } + + if temp.PrepTime != "" { + m.PrepTime = temp.PrepTime + } else if temp.PrepTimeOld != "" { + m.PrepTime = temp.PrepTimeOld + } + + if temp.CookTime != nil { + m.CookTime = temp.CookTime + } else if temp.CookTimeOld != nil { + m.CookTime = temp.CookTimeOld + } + + if temp.PerformTime != "" { + m.PerformTime = temp.PerformTime + } else if temp.PerformTimeOld != "" { + m.PerformTime = temp.PerformTimeOld + } + + m.Description = temp.Description + + if len(temp.RecipeCategory) > 0 { + m.RecipeCategory = temp.RecipeCategory + } else if len(temp.RecipeCategoryOld) > 0 { + m.RecipeCategory = temp.RecipeCategoryOld + } + + m.Tags = temp.Tags + m.Tools = temp.Tools + m.Rating = temp.Rating + + if temp.OrgURL != "" { + m.OrgURL = temp.OrgURL + } else if temp.OrgURLOld != "" { + m.OrgURL = temp.OrgURLOld + } + + if temp.DateAdded != "" { + m.DateAdded = temp.DateAdded + } else if temp.DateAddedOld != "" { + m.DateAdded = temp.DateAddedOld + } + + if temp.DateUpdated != "" { + m.DateUpdated = temp.DateUpdated + } else if temp.DateUpdatedOld != "" { + m.DateUpdated = temp.DateUpdatedOld + } + + if temp.CreatedAt != "" { + m.CreatedAt = temp.CreatedAt + } else if temp.CreatedAtOld != "" { + m.CreatedAt = temp.CreatedAtOld + } + + if temp.UpdateAt != "" { + m.UpdateAt = temp.UpdateAt + } else if temp.UpdateAtOld != "" { + m.UpdateAt = temp.UpdateAtOld + } + + m.LastMade = temp.LastMade + + if temp.RecipeIngredient != nil { + m.RecipeIngredient = temp.RecipeIngredient + } else if temp.RecipeIngredientOld != nil { + m.RecipeIngredient = temp.RecipeIngredientOld + } + + if temp.RecipeInstructions != nil { + m.RecipeInstructions = temp.RecipeInstructions + } else if temp.RecipeInstructionsOld != nil { + m.RecipeInstructions = temp.RecipeInstructionsOld + } + + m.Nutrition = temp.Nutrition + + return nil +} + +type recipeCategory struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +type recipeIngredient struct { + Quantity float64 `json:"quantity"` + Unit any `json:"unit"` + Food food `json:"food"` + Note string `json:"note"` + IsFood bool `json:"isFood"` + DisableAmount bool `json:"disable_amount"` + Display string `json:"display"` + Title any `json:"title"` + OriginalText string `json:"original_text"` + ReferenceID string `json:"reference_id"` +} + +func (r *recipeIngredient) UnmarshalJSON(data []byte) error { + var temp struct { + Quantity float64 `json:"quantity"` + Unit any `json:"unit"` + Food food `json:"food"` + Note string `json:"note"` + + IsFood bool `json:"isFood"` + IsFoodOld bool `json:"is_food"` + + DisableAmount bool `json:"disable_amount"` + DisableAmountOld bool `json:"disableAmount"` + + Display string `json:"display"` + Title any `json:"title"` + + OriginalText string `json:"original_text"` + OriginalTextOld string `json:"originalText"` + + ReferenceID string `json:"reference_id"` + ReferenceIDOld string `json:"referenceId"` + } + + err := json.Unmarshal(data, &temp) + if err != nil { + return err + } + + r.Quantity = temp.Quantity + r.Unit = temp.Unit + r.Food = temp.Food + r.Note = temp.Note + r.IsFood = temp.IsFood || temp.IsFoodOld + r.DisableAmount = temp.DisableAmount || temp.DisableAmountOld + r.Display = temp.Display + r.Title = temp.Title + + if temp.OriginalText != "" { + r.OriginalText = temp.OriginalText + } else if temp.OriginalTextOld != "" { + r.OriginalText = temp.OriginalTextOld + } + + if temp.ReferenceID != "" { + r.ReferenceID = temp.ReferenceID + } else if temp.ReferenceIDOld != "" { + r.ReferenceID = temp.ReferenceIDOld + } + return nil +} + +type food struct { + ID string `json:"id"` + Name string `json:"name"` + PluralName any `json:"plural_name"` + Description string `json:"description"` + Extras struct { + } `json:"extras"` + LabelID any `json:"label_id"` + Aliases []any `json:"aliases"` + Label any `json:"label"` + CreatedAt string `json:"created_at"` + UpdateAt string `json:"update_at"` +} + +func (f *food) UnmarshalJSON(data []byte) error { + var temp struct { ID string `json:"id"` Name string `json:"name"` - Slug string `json:"slug"` - } `json:"tags"` - Tools []models.HowToItem `json:"tools"` - Rating int `json:"rating"` - OrgURL string `json:"orgURL"` - DateAdded string `json:"dateAdded"` - DateUpdated string `json:"dateUpdated"` - CreatedAt string `json:"createdAt"` - UpdateAt string `json:"updateAt"` - RecipeIngredient []struct { - Quantity float64 `json:"quantity"` - Unit interface{} `json:"unit"` - Food struct { - ID string `json:"id"` - Name string `json:"name"` - PluralName interface{} `json:"pluralName"` - Description string `json:"description"` - Extras struct { - } `json:"extras"` - LabelID interface{} `json:"labelId"` - Aliases []interface{} `json:"aliases"` - Label interface{} `json:"label"` - CreatedAt string `json:"createdAt"` - UpdateAt string `json:"updateAt"` - } `json:"food"` - Note string `json:"note"` - IsFood bool `json:"isFood"` - DisableAmount bool `json:"disableAmount"` - Display string `json:"display"` - Title interface{} `json:"title"` - OriginalText string `json:"originalText"` - ReferenceID string `json:"referenceId"` - } `json:"recipeIngredient"` - RecipeInstructions []struct { - Title string `json:"title"` - Text string `json:"text"` - } `json:"recipeInstructions"` - Nutrition struct { - Calories string `json:"calories"` - FatContent string `json:"fatContent"` - ProteinContent string `json:"proteinContent"` - CarbohydrateContent string `json:"carbohydrateContent"` - FiberContent string `json:"fiberContent"` - SodiumContent string `json:"sodiumContent"` - SugarContent string `json:"sugarContent"` - } `json:"nutrition"` + + PluralName any `json:"plural_name"` + PluralNameOld any `json:"pluralName"` + + Description string `json:"description"` + Extras struct { + } `json:"extras"` + + LabelID any `json:"label_id"` + LabelIDOld any `json:"labelId"` + + Aliases []any `json:"aliases"` + Label any `json:"label"` + + CreatedAt string `json:"created_at"` + CreatedAtOld string `json:"createdAt"` + + UpdateAt string `json:"update_at"` + UpdateAtOld string `json:"updateAt"` + } + + err := json.Unmarshal(data, &temp) + if err != nil { + return err + } + + f.ID = temp.ID + f.Name = temp.Name + + if temp.PluralName != nil { + f.PluralName = temp.PluralName + } else if temp.PluralNameOld != nil { + f.PluralName = temp.PluralNameOld + } + + f.Description = temp.Description + f.Extras = temp.Extras + + if temp.LabelID != nil { + f.LabelID = temp.LabelID + } else if temp.LabelIDOld != nil { + f.LabelID = temp.LabelIDOld + } + + f.Aliases = temp.Aliases + f.Label = temp.Label + + if temp.CreatedAt != "" { + f.CreatedAt = temp.CreatedAt + } else if temp.CreatedAtOld != "" { + f.CreatedAt = temp.CreatedAtOld + } + + if temp.UpdateAt != "" { + f.UpdateAt = temp.UpdateAt + } else if temp.UpdateAtOld != "" { + f.UpdateAt = temp.UpdateAtOld + } + + return nil +} + +type recipeInstruction struct { + ID string `json:"id"` + Title string `json:"title"` + Summary string `json:"summary"` + Text string `json:"text"` + IngredientReferences []any `json:"ingredient_references"` +} + +type nutrition struct { + Calories string + CarbohydrateContent string + CholesterolContent string + FatContent string + FiberContent string + ProteinContent string + SaturatedFatContent string + SodiumContent string + SugarContent string + TransFatContent string + UnsaturatedFatContent string +} + +// Schema converts the Mealie nutrition struct to a NutritionSchema one. +func (n *nutrition) Schema() *models.NutritionSchema { + return &models.NutritionSchema{ + Calories: n.Calories, + Carbohydrates: n.CarbohydrateContent, + Cholesterol: n.CholesterolContent, + Fat: n.FatContent, + Fiber: n.FiberContent, + Protein: n.ProteinContent, + SaturatedFat: n.SaturatedFatContent, + Sodium: n.SodiumContent, + Sugar: n.SugarContent, + TransFat: n.TransFatContent, + UnsaturatedFat: n.UnsaturatedFatContent, + } +} + +func (n *nutrition) UnmarshalJSON(data []byte) error { + var temp struct { + Calories *string `json:"calories"` + + CarbohydrateContent *string `json:"carbohydrate_content"` + CarbohydrateContentOld *string `json:"carbohydrateContent"` + + CholesterolContent *string `json:"cholesterol_content"` + CholesterolContentOld *string `json:"cholesterolContent"` + + FatContent *string `json:"fat_content"` + FatContentOld *string `json:"fatContent"` + + FiberContent *string `json:"fiber_content"` + FiberContentOld *string `json:"fiberContent"` + + ProteinContent *string `json:"protein_content"` + ProteinContentOld *string `json:"proteinContent"` + + SaturatedFatContent *string `json:"saturated_fat_content"` + SaturatedFatContentOld *string `json:"saturatedFatContent"` + + SodiumContent *string `json:"sodium_content"` + SodiumContentOld *string `json:"sodiumContent"` + + SugarContent *string `json:"sugar_content"` + SugarContentOld *string `json:"sugarContent"` + + TransFatContent *string `json:"trans_fat_content"` + TransFatContentOld *string `json:"transFatContent"` + + UnsaturatedFatContent *string `json:"unsaturated_fat_content"` + UnsaturatedFatContentOld *string `json:"unsaturatedFatContent"` + } + + err := json.Unmarshal(data, &temp) + if err != nil { + return err + } + + if temp.Calories != nil { + n.Calories = *temp.Calories + } + + if temp.CarbohydrateContent != nil { + n.CarbohydrateContent = *temp.CarbohydrateContent + } else if temp.CarbohydrateContentOld != nil { + n.CarbohydrateContent = *temp.CarbohydrateContentOld + } + + if temp.CholesterolContent != nil { + n.CholesterolContent = *temp.CholesterolContent + } else if temp.CholesterolContentOld != nil { + n.CholesterolContent = *temp.CholesterolContentOld + } + + if temp.FatContent != nil { + n.FatContent = *temp.FatContent + } else if temp.FatContentOld != nil { + n.FatContent = *temp.FatContentOld + } + + if temp.FiberContent != nil { + n.FiberContent = *temp.FiberContent + } else if temp.FiberContentOld != nil { + n.FiberContent = *temp.FiberContentOld + } + + if temp.ProteinContent != nil { + n.ProteinContent = *temp.ProteinContent + } else if temp.ProteinContentOld != nil { + n.ProteinContent = *temp.ProteinContentOld + } + + if temp.SaturatedFatContent != nil { + n.SaturatedFatContent = *temp.SaturatedFatContent + } else if temp.SaturatedFatContentOld != nil { + n.SaturatedFatContent = *temp.SaturatedFatContentOld + } + + if temp.SodiumContent != nil { + n.SodiumContent = *temp.SodiumContent + } else if temp.SodiumContentOld != nil { + n.SodiumContent = *temp.SodiumContentOld + } + + if temp.SugarContent != nil { + n.SugarContent = *temp.SugarContent + } else if temp.SugarContentOld != nil { + n.SugarContent = *temp.SugarContentOld + } + + if temp.TransFatContent != nil { + n.TransFatContent = *temp.TransFatContent + } else if temp.TransFatContentOld != nil { + n.TransFatContent = *temp.TransFatContentOld + } + + if temp.UnsaturatedFatContent != nil { + n.UnsaturatedFatContent = *temp.UnsaturatedFatContent + } else if temp.UnsaturatedFatContentOld != nil { + n.UnsaturatedFatContent = *temp.UnsaturatedFatContentOld + } + + return nil } // MealieImport imports recipes from a Mealie instance. @@ -246,7 +773,7 @@ func MealieImport(baseURL, username, password string, client *http.Client, uploa return } - var m mealieRecipeResponse + var m MealieRecipe err = json.NewDecoder(res.Body).Decode(&m) if err != nil { _ = res.Body.Close() @@ -284,6 +811,19 @@ func MealieImport(baseURL, username, password string, client *http.Client, uploa } } + times := models.Times{ + Prep: duration.From(m.PrepTime), + Cook: duration.From(m.PerformTime), + } + + if m.CookTime == nil && m.PrepTime != "" { + if m.TotalTime != "" { + times.Cook = duration.From(m.TotalTime) - duration.From(m.PrepTime) + } else if m.PerformTime != "" { + times.Cook = duration.From(m.PerformTime) - duration.From(m.PrepTime) + } + } + ingredients := make([]string, 0, len(m.RecipeIngredient)) for _, s := range m.RecipeIngredient { var v string @@ -351,18 +891,15 @@ func MealieImport(baseURL, username, password string, client *http.Client, uploa Keywords: keywords, Name: m.Name, Nutrition: models.Nutrition{ - Calories: extractNut(m.Nutrition.Calories, " kcal"), - Fiber: extractNut(m.Nutrition.FiberContent, "g"), - Protein: extractNut(m.Nutrition.ProteinContent, "g"), - Sodium: extractNut(m.Nutrition.SodiumContent, "g"), - Sugars: extractNut(m.Nutrition.SugarContent, "g"), - TotalCarbohydrates: extractNut(m.Nutrition.CarbohydrateContent, "g"), - TotalFat: extractNut(m.Nutrition.FatContent, "g"), - }, - Times: models.Times{ - Prep: duration.From(m.PrepTime), - Cook: duration.From(m.PerformTime), + Calories: m.Nutrition.Calories, + Fiber: m.Nutrition.FiberContent, + Protein: m.Nutrition.ProteinContent, + Sodium: m.Nutrition.SodiumContent, + Sugars: m.Nutrition.SugarContent, + TotalCarbohydrates: m.Nutrition.CarbohydrateContent, + TotalFat: m.Nutrition.FatContent, }, + Times: times, Tools: m.Tools, UpdatedAt: dateModified, URL: source, @@ -378,10 +915,3 @@ func MealieImport(baseURL, username, password string, client *http.Client, uploa wg.Wait() return recipes, nil } - -func extractNut(v, unit string) string { - if v == "" { - return "" - } - return v + unit -} diff --git a/internal/integrations/mealie_test.go b/internal/integrations/mealie_test.go index 05e92b83..3adbbf19 100644 --- a/internal/integrations/mealie_test.go +++ b/internal/integrations/mealie_test.go @@ -12,6 +12,109 @@ import ( ) func TestMealieImport(t *testing.T) { + testcases := []struct { + name string + mealieJSON string + wantRecipe models.Recipe + }{ + { + name: "old API", + mealieJSON: `{"id":"843b4a6d-6855-48c3-8186-22f096310243","userId":"e72ff251-4693-4e44-ad1d-9d9c2b033541","groupId":"083bba0c-e400-4b84-8055-b01a888b27fd","name":"Roasted Vegetable Bowls with Green Tahini","slug":"roasted-vegetable-bowls-with-green-tahini","image":"Z4Ox","recipeYield":"6 servings","totalTime":"45 minutes","prepTime":"15 minutes","cookTime":null,"performTime":"30 minutes","description":"Roasted Vegetable Bowls! Crispy tender roasted veggies, buttery avocado, all together in a bowl with a drizzle of green tahini sauce.","recipeCategory":[],"tags":[{"id":"70cc7ab9-cc6f-41d0-b8b9-8d16384f857e","name":"Vegetable Bowl Recipe","slug":"vegetable-bowl-recipe"},{"id":"da629ccc-56cb-4400-bce1-55ca0f14905b","name":"Roasted Vegetable Bowls","slug":"roasted-vegetable-bowls"},{"id":"e3184b1f-2bd0-48b8-b766-fa3eaf8285a5","name":"Green Tahini","slug":"green-tahini"}],"tools":[],"rating":4,"orgURL":"https://pinchofyum.com/30-minute-meal-prep-roasted-vegetable-bowls-with-green-tahini","dateAdded":"2024-04-12","dateUpdated":"2024-04-12T18:14:29.168064","createdAt":"2024-04-12T18:06:06.692275","updateAt":"2024-04-12T18:07:56.850947","lastMade":null,"recipeIngredient":[{"quantity":8.0,"unit":null,"food":{"id":"31d502b5-dea9-4580-b8b2-86bfde80f456","name":"carrot","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.825124","updateAt":"2024-03-05T06:54:56.825126"},"note":"large, peeled and chopped","isFood":true,"disableAmount":false,"display":"8 carrot large, peeled and chopped","title":null,"originalText":"8 large carrots, peeled and chopped","referenceId":"0b7f622f-a6f1-4e51-b381-d665ee54da47"},{"quantity":3.0,"unit":null,"food":null,"note":"chopped","isFood":true,"disableAmount":false,"display":"3 chopped","title":null,"originalText":"3 golden potatoes, chopped","referenceId":"d0944e99-f189-48aa-b951-1d2c1aaf7655"},{"quantity":1.0,"unit":null,"food":{"id":"05bb0bf7-dc26-4961-9bce-bd5563f7a6c7","name":"broccoli","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.552679","updateAt":"2024-03-05T06:54:56.552681"},"note":"cut into florets","isFood":true,"disableAmount":false,"display":"1 broccoli cut into florets","title":null,"originalText":"1 head of broccoli, cut into florets","referenceId":"3b1e5c67-5f02-49b8-88d7-6db8a681de05"},{"quantity":1.0,"unit":null,"food":{"id":"1a0beaa6-b6f2-4143-81e9-6709fe00d33a","name":"cauliflower","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.214977","updateAt":"2024-03-05T06:54:56.214981"},"note":"cut into florets","isFood":true,"disableAmount":false,"display":"1 cauliflower cut into florets","title":null,"originalText":"1 head of cauliflower, cut into florets","referenceId":"c0836160-4afd-4157-a780-7ac0a7f41a6f"},{"quantity":0.0,"unit":null,"food":null,"note":"","isFood":true,"disableAmount":false,"display":"","title":null,"originalText":"olive oil and salt","referenceId":"ac21685f-510d-4534-b4cd-e0ac57e04a04"},{"quantity":0.5,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"02bc4201-08ca-45b2-b032-7babfa4346f4","name":"olive oil","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.572704","updateAt":"2024-03-05T06:54:56.572706"},"note":"mild tasting)","isFood":true,"disableAmount":false,"display":"¹/₂ cup olive oil mild tasting)","title":null,"originalText":"1/2 cup olive oil (mild tasting)","referenceId":"af426c53-d5e4-4ab0-bc17-6b80ab2d389d"},{"quantity":0.5,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"c30d5cf5-d7e6-4f8b-8338-a010cae94441","name":"water","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:59:33.037532","updateAt":"2024-03-05T06:59:33.037536"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₂ cup water","title":null,"originalText":"1/2 cup water","referenceId":"5cc3edfb-11c3-403f-afc0-f10d7863791c"},{"quantity":0.25,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"337615d4-f263-4289-80e2-7fe79983c29e","name":"tahini","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.942480","updateAt":"2024-03-05T06:54:56.942482"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₄ cup tahini","title":null,"originalText":"1/4 cup tahini","referenceId":"e77a5636-f06c-479d-932e-86920ff04ae3"},{"quantity":0.0,"unit":null,"food":{"id":"3fce1ca1-3fbe-4a29-b0c8-6411a581be4d","name":"cilantro","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.640255","updateAt":"2024-03-05T06:54:56.640257"},"note":"and/or parsley","isFood":true,"disableAmount":false,"display":"cilantro and/or parsley","title":null,"originalText":"a big bunch of cilantro and/or parsley","referenceId":"eee40c2c-ce2d-42db-abd7-2743db1a017e"},{"quantity":1.0,"unit":{"id":"b9ca3f9e-f7d5-4bce-8181-29ae56939ce6","name":"clove","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T08:19:26.421964","updateAt":"2024-03-05T08:19:26.421968"},"food":{"id":"7f2f8ad6-035d-46c7-8503-9e6bf7281165","name":"garlic","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.355030","updateAt":"2024-03-05T06:54:56.355032"},"note":"","isFood":true,"disableAmount":false,"display":"1 clove garlic","title":null,"originalText":"1 clove garlic","referenceId":"84153160-0675-49f6-9363-0f255b0fdf1c"},{"quantity":0.0,"unit":null,"food":null,"note":"(about 2 tablespoons)","isFood":true,"disableAmount":false,"display":"(about 2 tablespoons)","title":null,"originalText":"squeeze of half a lemon (about 2 tablespoons)","referenceId":"777d50a9-ad50-4b03-a5e8-331ae6cc94f1"},{"quantity":0.5,"unit":{"id":"cda9b5eb-21c5-4acf-b65f-7b397e560eb3","name":"teaspoon","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T08:19:34.732459","updateAt":"2024-03-05T08:19:34.732462"},"food":{"id":"90a64fd5-ce9d-4774-a1c6-68c65ed5afea","name":"salt","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.265153","updateAt":"2024-03-05T06:54:56.265155"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₂ teaspoon salt","title":null,"originalText":"1/2 teaspoon salt (more to taste)","referenceId":"cd6dc996-55bf-40a7-a280-c6b754157f2d"},{"quantity":6.0,"unit":null,"food":{"id":"b72ab124-26bc-40f1-a59c-3d469fe890b1","name":"eggs","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.442542","updateAt":"2024-03-05T06:54:56.442544"},"note":"hard boiled (or other protein)","isFood":true,"disableAmount":false,"display":"6 eggs hard boiled (or other protein)","title":null,"originalText":"6 hard boiled eggs (or other protein)","referenceId":"b56905f5-1d19-4d20-8498-3fd9285bf8c3"},{"quantity":3.0,"unit":null,"food":{"id":"d20ca2a2-f869-4680-937a-e00349b34893","name":"avocado","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.787485","updateAt":"2024-03-05T06:54:56.787488"},"note":"","isFood":true,"disableAmount":false,"display":"3 avocado","title":null,"originalText":"3 avocados","referenceId":"4e2cbbd8-43b0-4f37-8247-00b547ae99e3"}],"recipeInstructions":[{"id":"4f6932fd-1da1-4264-a7b3-fc8c7f8bcec1","title":"","text":"Prep","ingredientReferences":[]},{"id":"668767d9-1f73-4283-b450-fed81bb9521a","title":"","text":"Preheat the oven to 425 degrees.","ingredientReferences":[]},{"id":"ce35bb0f-aac7-4396-b4c6-73e28a8bf1bc","title":"","text":"Roasted Vegetables","ingredientReferences":[]},{"id":"cc60b45f-014c-4323-9767-4e3e957dead0","title":"","text":"Arrange your vegetables onto a few baking sheets lined with parchment (I keep each vegetable in its own little section). Toss with olive oil and salt. Roast for 25-30 minutes.","ingredientReferences":[]},{"id":"517eb65d-dc49-4d63-9a2d-82dbdee8f728","title":"","text":"Sauce","ingredientReferences":[]},{"id":"039c6817-4e7f-4690-9c84-20574bec62a4","title":"","text":"While the veggies are roasting, blitz up your sauce in the food processor or blender.","ingredientReferences":[]},{"id":"a78f9737-4b5b-4798-aa48-9f087d63c535","title":"","text":"Finish","ingredientReferences":[]},{"id":"ade3a45d-1546-47ca-ab0b-441707c58429","title":"","text":"Voila! Portion and save for the week! Serve with avocado or hard boiled eggs or… anything else that would make your lunch life amazing.","ingredientReferences":[]}],"nutrition":{"calories":"322","fatContent":"24.6","proteinContent":"6.1","carbohydrateContent":"24.7","fiberContent":"6.7","sodiumContent":"302.3","sugarContent":"6.9"},"settings":{"public":true,"showNutrition":false,"showAssets":false,"landscapeView":false,"disableComments":false,"disableAmount":false,"locked":false},"assets":[],"notes":[],"extras":{},"comments":[]}`, + wantRecipe: models.Recipe{ + Category: "uncategorized", + CreatedAt: time.Date(2024, 4, 12, 0, 0, 0, 0, time.UTC), + Description: "Roasted Vegetable Bowls! Crispy tender roasted veggies, buttery avocado, all together in a bowl with a drizzle of green tahini sauce.", + Images: []uuid.UUID{uuid.MustParse("f2f4b3aa-1e04-42a2-a581-607bf84f6800")}, + Ingredients: []string{ + "8 large carrots, peeled and chopped", "3 golden potatoes, chopped", + "1 head of broccoli, cut into florets", + "1 head of cauliflower, cut into florets", "olive oil and salt", + "1/2 cup olive oil (mild tasting)", "1/2 cup water", "1/4 cup tahini", + "a big bunch of cilantro and/or parsley", + "1 clove garlic", + "squeeze of half a lemon (about 2 tablespoons)", + "1/2 teaspoon salt (more to taste)", + "6 hard boiled eggs (or other protein)", + "3 avocados", + }, + Instructions: []string{ + "Prep", "Preheat the oven to 425 degrees.", "Roasted Vegetables", + "Arrange your vegetables onto a few baking sheets lined with parchment (I keep each vegetable in its own little section). Toss with olive oil and salt. Roast for 25-30 minutes.", "Sauce", + "While the veggies are roasting, blitz up your sauce in the food processor or blender.", + "Finish", + "Voila! Portion and save for the week! Serve with avocado or hard boiled eggs or… anything else that would make your lunch life amazing.", + }, + Keywords: []string{"Vegetable Bowl Recipe", "Roasted Vegetable Bowls", "Green Tahini"}, + Name: "Roasted Vegetable Bowls with Green Tahini", + Nutrition: models.Nutrition{ + Calories: "322", + Fiber: "6.7", + Protein: "6.1", + Sodium: "302.3", + Sugars: "6.9", + TotalCarbohydrates: "24.7", + TotalFat: "24.6", + }, + Times: models.Times{Prep: 15 * time.Minute, Cook: 30 * time.Minute}, + Tools: []models.HowToItem{}, + UpdatedAt: time.Date(2024, 04, 12, 0, 0, 0, 0, time.UTC), + URL: "https://pinchofyum.com/30-minute-meal-prep-roasted-vegetable-bowls-with-green-tahini", + Yield: 6, + }, + }, + { + name: "latest API", + mealieJSON: `{"id":"134372c8-1b49-41e5-a2d2-3249cc7bdb12","user_id":"499aa092-e3fe-46e7-ac5e-9ddf360c9eee","household_id":"67858612-ba5b-470c-bd0d-20b7fdf9a88a","group_id":"0d7fc2c1-58f4-4c88-8177-083263e467c4","name":"Mini cannelés chorizo comté au thermomix","slug":"mini-canneles-chorizo-comte-au-thermomix","image":"wTJi","recipe_servings":24.0,"recipe_yield_quantity":0.0,"recipe_yield":"","total_time":null,"prep_time":"15 minutes","cook_time":null,"perform_time":"54 minutes","description":"Qui ne connait pas le cannelé, ce petit gâteau Bordelais irresistible à pâte molle et tendre. Le voici revisité en version salée pour un apéro légèrement relevé, avec du chorizo et du comté.\r\n\nL'essayer c'est l'adopter :)","recipe_category":[],"tags":[],"tools":[],"rating":4.0,"org_url":"https://www.cookomix.com/recettes/mini-canneles-chorizo-comte-thermomix/","date_added":"2024-12-18","date_updated":"2024-12-31T10:20:45.610286Z","created_at":"2024-12-18T08:18:02.011499Z","updated_at":"2024-12-31T10:20:45.622388Z","last_made":null,"recipe_ingredient":[{"quantity":1.0,"unit":null,"food":null,"note":"Beurre - 30 grammes","is_food":false,"disable_amount":true,"display":"Beurre - 30 grammes","title":null,"original_text":null,"reference_id":"1c728d6b-fd56-4516-a83a-1e742163d879"},{"quantity":1.0,"unit":null,"food":null,"note":"Lait demi-écrémé - 250 ml","is_food":false,"disable_amount":true,"display":"Lait demi-écrémé - 250 ml","title":null,"original_text":null,"reference_id":"e5ea6b2d-9fb3-4c21-97c8-76c0f46ce407"},{"quantity":1.0,"unit":null,"food":null,"note":"Chorizo - 50 grammes","is_food":false,"disable_amount":true,"display":"Chorizo - 50 grammes","title":null,"original_text":null,"reference_id":"04ae6888-705b-4cb7-be99-bf747248422c"},{"quantity":1.0,"unit":null,"food":null,"note":"Comté - 60 grammes","is_food":false,"disable_amount":true,"display":"Comté - 60 grammes","title":null,"original_text":null,"reference_id":"2ebad54d-ab1a-41e6-87af-84196fc7cbec"},{"quantity":1.0,"unit":null,"food":null,"note":"Oeuf - 2","is_food":false,"disable_amount":true,"display":"Oeuf - 2","title":null,"original_text":null,"reference_id":"a621f5f7-5b78-4a63-9564-cc4df260d0f2"},{"quantity":1.0,"unit":null,"food":null,"note":"Farine - 60 grammes","is_food":false,"disable_amount":true,"display":"Farine - 60 grammes","title":null,"original_text":null,"reference_id":"cd53e2eb-8163-4c84-a593-8fcc025a1431"},{"quantity":1.0,"unit":null,"food":null,"note":"Sel - 1 pincée","is_food":false,"disable_amount":true,"display":"Sel - 1 pincée","title":null,"original_text":null,"reference_id":"2928a65b-12be-4ee1-9c6a-32de40c2917e"},{"quantity":1.0,"unit":null,"food":null,"note":"Poivre - 1 pincée","is_food":false,"disable_amount":true,"display":"Poivre - 1 pincée","title":null,"original_text":null,"reference_id":"e42c649c-12ad-44e2-a4b8-87387442f2ff"}],"recipe_instructions":[{"id":"02e8a3ea-1696-4adc-8282-a96be0df0aec","title":"","summary":"","text":"Préchauffer le four à 180°C.","ingredient_references":[]},{"id":"6a841504-b4ad-408a-9281-0acb48c1573e","title":"","summary":"","text":"Dans une casserolle mettre 30 grammes de beurre coupés en morceaux","ingredient_references":[]},{"id":"a67f84b0-3960-4776-b01f-fba47aa20738","title":"","summary":"","text":"Ajouter le lait","ingredient_references":[]},{"id":"a4f49ac8-1280-4c6b-8146-2b62d5b3a706","title":"","summary":"","text":"Faire chauffer à feu moyen en tournant régulièrement","ingredient_references":[]},{"id":"92adc312-0070-4f74-8e1f-6684c352fbfe","title":"","summary":"","text":"Dans un saladier, ajouter 1 oeuf et le jaune d'oeuf","ingredient_references":[]},{"id":"2aa1d7c3-31aa-41f2-a628-c518b72f604e","title":"","summary":"","text":"Ajouter 60 grammes de farine","ingredient_references":[]},{"id":"f77dba58-d336-470e-8232-cbe5cca1a6bc","title":"","summary":"","text":"Ajouter 1 pincée de sel (à ajuster en fonction des goûts)","ingredient_references":[]},{"id":"b4fff326-6e32-4edf-8515-34af3f6b17c6","title":"","summary":"","text":"Ajouter 1 pincée de poivre (à ajuster en fonction des goûts) ","ingredient_references":[]},{"id":"cb4ac495-af3d-4bca-8452-af6631ac31e9","title":"","summary":"","text":"Mélanger pour obtenir une pâte lisse","ingredient_references":[]},{"id":"88e3cc68-74f5-4800-9b57-e6530cfda543","title":"","summary":"","text":"Ajouter progressivement le mélange lait/beurre","ingredient_references":[]},{"id":"9a6c5dae-d4cf-4edd-aadf-c776b5db415a","title":"","summary":"","text":"Ajouter le comté et le chorizo coupés en dés. Bien mélanger","ingredient_references":[]},{"id":"e63371c4-cbfb-4108-a4c3-bd7ec07eebd4","title":"","summary":"","text":"Transvaser dans des moules à mini-cannelés en remplissant aux 3/4.\nLa quantité doit permettre de faire 2 fournées de 12.","ingredient_references":[]},{"id":"d97c56d4-a7a6-4237-9c15-ccf64f715903","title":"","summary":"","text":"Mettre dans le four pendant 25 min à 180°C. Mode chaleur tournante. Adaptez la cuisson en fonction de vos moules et/ou de votre four.","ingredient_references":[]}],"nutrition":{"calories":"37","carbohydrate_content":null,"cholesterol_content":null,"fat_content":null,"fiber_content":null,"protein_content":null,"saturated_fat_content":null,"sodium_content":null,"sugar_content":null,"trans_fat_content":null,"unsaturated_fat_content":null},"settings":{"public":true,"show_nutrition":false,"show_assets":false,"landscape_view":false,"disable_comments":false,"disable_amount":true,"locked":false},"assets":[],"notes":[],"extras":{},"comments":[]}`, + wantRecipe: models.Recipe{ + Category: "uncategorized", + CreatedAt: time.Date(2024, 12, 18, 0, 0, 0, 0, time.UTC), + Description: "Qui ne connait pas le cannelé, ce petit gâteau Bordelais irresistible à pâte molle et tendre. Le voici revisité en version salée pour un apéro légèrement relevé, avec du chorizo et du comté.\r\n\nL'essayer c'est l'adopter :)", + Images: []uuid.UUID{uuid.MustParse("f2f4b3aa-1e04-42a2-a581-607bf84f6800")}, + Ingredients: []string{ + "Beurre - 30 grammes", "Lait demi-écrémé - 250 ml", "Chorizo - 50 grammes", + "Comté - 60 grammes", "Oeuf - 2", "Farine - 60 grammes", "Sel - 1 pincée", + "Poivre - 1 pincée", + }, + Instructions: []string{ + "Préchauffer le four à 180°C.", + "Dans une casserolle mettre 30 grammes de beurre coupés en morceaux", + "Ajouter le lait", "Faire chauffer à feu moyen en tournant régulièrement", + "Dans un saladier, ajouter 1 oeuf et le jaune d'oeuf", + "Ajouter 60 grammes de farine", + "Ajouter 1 pincée de sel (à ajuster en fonction des goûts)", + "Ajouter 1 pincée de poivre (à ajuster en fonction des goûts) ", + "Mélanger pour obtenir une pâte lisse", + "Ajouter progressivement le mélange lait/beurre", + "Ajouter le comté et le chorizo coupés en dés. Bien mélanger", + "Transvaser dans des moules à mini-cannelés en remplissant aux 3/4.\nLa quantité doit permettre de faire 2 fournées de 12.", + "Mettre dans le four pendant 25 min à 180°C. Mode chaleur tournante. Adaptez la cuisson en fonction de vos moules et/ou de votre four.", + }, + Keywords: []string{}, + Name: "Mini cannelés chorizo comté au thermomix", + Nutrition: models.Nutrition{ + Calories: "37", + }, + Times: models.Times{ + Prep: 15 * time.Minute, + Cook: 39 * time.Minute, + }, + Tools: []models.HowToItem{}, + UpdatedAt: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), + URL: "https://www.cookomix.com/recettes/mini-canneles-chorizo-comte-thermomix/", + Yield: 24, + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + testMealie(t, tc.mealieJSON, tc.wantRecipe) + }) + } +} + +func testMealie(t testing.TB, mealieRecipe string, wantRecipe models.Recipe) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/auth/token": @@ -24,9 +127,8 @@ func TestMealieImport(t *testing.T) { _, _ = w.Write([]byte(data)) default: if strings.HasPrefix(r.URL.Path, "/api/recipes/") { - data := `{"id":"843b4a6d-6855-48c3-8186-22f096310243","userId":"e72ff251-4693-4e44-ad1d-9d9c2b033541","groupId":"083bba0c-e400-4b84-8055-b01a888b27fd","name":"Roasted Vegetable Bowls with Green Tahini","slug":"roasted-vegetable-bowls-with-green-tahini","image":"Z4Ox","recipeYield":"6 servings","totalTime":"45 minutes","prepTime":"15 minutes","cookTime":null,"performTime":"30 minutes","description":"Roasted Vegetable Bowls! Crispy tender roasted veggies, buttery avocado, all together in a bowl with a drizzle of green tahini sauce.","recipeCategory":[],"tags":[{"id":"70cc7ab9-cc6f-41d0-b8b9-8d16384f857e","name":"Vegetable Bowl Recipe","slug":"vegetable-bowl-recipe"},{"id":"da629ccc-56cb-4400-bce1-55ca0f14905b","name":"Roasted Vegetable Bowls","slug":"roasted-vegetable-bowls"},{"id":"e3184b1f-2bd0-48b8-b766-fa3eaf8285a5","name":"Green Tahini","slug":"green-tahini"}],"tools":[],"rating":4,"orgURL":"https://pinchofyum.com/30-minute-meal-prep-roasted-vegetable-bowls-with-green-tahini","dateAdded":"2024-04-12","dateUpdated":"2024-04-12T18:14:29.168064","createdAt":"2024-04-12T18:06:06.692275","updateAt":"2024-04-12T18:07:56.850947","lastMade":null,"recipeIngredient":[{"quantity":8.0,"unit":null,"food":{"id":"31d502b5-dea9-4580-b8b2-86bfde80f456","name":"carrot","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.825124","updateAt":"2024-03-05T06:54:56.825126"},"note":"large, peeled and chopped","isFood":true,"disableAmount":false,"display":"8 carrot large, peeled and chopped","title":null,"originalText":"8 large carrots, peeled and chopped","referenceId":"0b7f622f-a6f1-4e51-b381-d665ee54da47"},{"quantity":3.0,"unit":null,"food":null,"note":"chopped","isFood":true,"disableAmount":false,"display":"3 chopped","title":null,"originalText":"3 golden potatoes, chopped","referenceId":"d0944e99-f189-48aa-b951-1d2c1aaf7655"},{"quantity":1.0,"unit":null,"food":{"id":"05bb0bf7-dc26-4961-9bce-bd5563f7a6c7","name":"broccoli","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.552679","updateAt":"2024-03-05T06:54:56.552681"},"note":"cut into florets","isFood":true,"disableAmount":false,"display":"1 broccoli cut into florets","title":null,"originalText":"1 head of broccoli, cut into florets","referenceId":"3b1e5c67-5f02-49b8-88d7-6db8a681de05"},{"quantity":1.0,"unit":null,"food":{"id":"1a0beaa6-b6f2-4143-81e9-6709fe00d33a","name":"cauliflower","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.214977","updateAt":"2024-03-05T06:54:56.214981"},"note":"cut into florets","isFood":true,"disableAmount":false,"display":"1 cauliflower cut into florets","title":null,"originalText":"1 head of cauliflower, cut into florets","referenceId":"c0836160-4afd-4157-a780-7ac0a7f41a6f"},{"quantity":0.0,"unit":null,"food":null,"note":"","isFood":true,"disableAmount":false,"display":"","title":null,"originalText":"olive oil and salt","referenceId":"ac21685f-510d-4534-b4cd-e0ac57e04a04"},{"quantity":0.5,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"02bc4201-08ca-45b2-b032-7babfa4346f4","name":"olive oil","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.572704","updateAt":"2024-03-05T06:54:56.572706"},"note":"mild tasting)","isFood":true,"disableAmount":false,"display":"¹/₂ cup olive oil mild tasting)","title":null,"originalText":"1/2 cup olive oil (mild tasting)","referenceId":"af426c53-d5e4-4ab0-bc17-6b80ab2d389d"},{"quantity":0.5,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"c30d5cf5-d7e6-4f8b-8338-a010cae94441","name":"water","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:59:33.037532","updateAt":"2024-03-05T06:59:33.037536"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₂ cup water","title":null,"originalText":"1/2 cup water","referenceId":"5cc3edfb-11c3-403f-afc0-f10d7863791c"},{"quantity":0.25,"unit":{"id":"56939576-ff3a-4760-98c4-cd7e7ea8418b","name":"cup","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T06:59:19.679450","updateAt":"2024-03-05T06:59:19.679454"},"food":{"id":"337615d4-f263-4289-80e2-7fe79983c29e","name":"tahini","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.942480","updateAt":"2024-03-05T06:54:56.942482"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₄ cup tahini","title":null,"originalText":"1/4 cup tahini","referenceId":"e77a5636-f06c-479d-932e-86920ff04ae3"},{"quantity":0.0,"unit":null,"food":{"id":"3fce1ca1-3fbe-4a29-b0c8-6411a581be4d","name":"cilantro","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.640255","updateAt":"2024-03-05T06:54:56.640257"},"note":"and/or parsley","isFood":true,"disableAmount":false,"display":"cilantro and/or parsley","title":null,"originalText":"a big bunch of cilantro and/or parsley","referenceId":"eee40c2c-ce2d-42db-abd7-2743db1a017e"},{"quantity":1.0,"unit":{"id":"b9ca3f9e-f7d5-4bce-8181-29ae56939ce6","name":"clove","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T08:19:26.421964","updateAt":"2024-03-05T08:19:26.421968"},"food":{"id":"7f2f8ad6-035d-46c7-8503-9e6bf7281165","name":"garlic","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.355030","updateAt":"2024-03-05T06:54:56.355032"},"note":"","isFood":true,"disableAmount":false,"display":"1 clove garlic","title":null,"originalText":"1 clove garlic","referenceId":"84153160-0675-49f6-9363-0f255b0fdf1c"},{"quantity":0.0,"unit":null,"food":null,"note":"(about 2 tablespoons)","isFood":true,"disableAmount":false,"display":"(about 2 tablespoons)","title":null,"originalText":"squeeze of half a lemon (about 2 tablespoons)","referenceId":"777d50a9-ad50-4b03-a5e8-331ae6cc94f1"},{"quantity":0.5,"unit":{"id":"cda9b5eb-21c5-4acf-b65f-7b397e560eb3","name":"teaspoon","pluralName":null,"description":"","extras":{},"fraction":true,"abbreviation":"","pluralAbbreviation":"","useAbbreviation":false,"aliases":[],"createdAt":"2024-03-05T08:19:34.732459","updateAt":"2024-03-05T08:19:34.732462"},"food":{"id":"90a64fd5-ce9d-4774-a1c6-68c65ed5afea","name":"salt","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.265153","updateAt":"2024-03-05T06:54:56.265155"},"note":"","isFood":true,"disableAmount":false,"display":"¹/₂ teaspoon salt","title":null,"originalText":"1/2 teaspoon salt (more to taste)","referenceId":"cd6dc996-55bf-40a7-a280-c6b754157f2d"},{"quantity":6.0,"unit":null,"food":{"id":"b72ab124-26bc-40f1-a59c-3d469fe890b1","name":"eggs","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.442542","updateAt":"2024-03-05T06:54:56.442544"},"note":"hard boiled (or other protein)","isFood":true,"disableAmount":false,"display":"6 eggs hard boiled (or other protein)","title":null,"originalText":"6 hard boiled eggs (or other protein)","referenceId":"b56905f5-1d19-4d20-8498-3fd9285bf8c3"},{"quantity":3.0,"unit":null,"food":{"id":"d20ca2a2-f869-4680-937a-e00349b34893","name":"avocado","pluralName":null,"description":"","extras":{},"labelId":null,"aliases":[],"label":null,"createdAt":"2024-03-05T06:54:56.787485","updateAt":"2024-03-05T06:54:56.787488"},"note":"","isFood":true,"disableAmount":false,"display":"3 avocado","title":null,"originalText":"3 avocados","referenceId":"4e2cbbd8-43b0-4f37-8247-00b547ae99e3"}],"recipeInstructions":[{"id":"4f6932fd-1da1-4264-a7b3-fc8c7f8bcec1","title":"","text":"Prep","ingredientReferences":[]},{"id":"668767d9-1f73-4283-b450-fed81bb9521a","title":"","text":"Preheat the oven to 425 degrees.","ingredientReferences":[]},{"id":"ce35bb0f-aac7-4396-b4c6-73e28a8bf1bc","title":"","text":"Roasted Vegetables","ingredientReferences":[]},{"id":"cc60b45f-014c-4323-9767-4e3e957dead0","title":"","text":"Arrange your vegetables onto a few baking sheets lined with parchment (I keep each vegetable in its own little section). Toss with olive oil and salt. Roast for 25-30 minutes.","ingredientReferences":[]},{"id":"517eb65d-dc49-4d63-9a2d-82dbdee8f728","title":"","text":"Sauce","ingredientReferences":[]},{"id":"039c6817-4e7f-4690-9c84-20574bec62a4","title":"","text":"While the veggies are roasting, blitz up your sauce in the food processor or blender.","ingredientReferences":[]},{"id":"a78f9737-4b5b-4798-aa48-9f087d63c535","title":"","text":"Finish","ingredientReferences":[]},{"id":"ade3a45d-1546-47ca-ab0b-441707c58429","title":"","text":"Voila! Portion and save for the week! Serve with avocado or hard boiled eggs or… anything else that would make your lunch life amazing.","ingredientReferences":[]}],"nutrition":{"calories":"322","fatContent":"24.6","proteinContent":"6.1","carbohydrateContent":"24.7","fiberContent":"6.7","sodiumContent":"302.3","sugarContent":"6.9"},"settings":{"public":true,"showNutrition":false,"showAssets":false,"landscapeView":false,"disableComments":false,"disableAmount":false,"locked":false},"assets":[],"notes":[],"extras":{},"comments":[]}` w.WriteHeader(200) - _, _ = w.Write([]byte(data)) + _, _ = w.Write([]byte(mealieRecipe)) } else if strings.HasPrefix(r.URL.Path, "/api/media/recipes/") { w.WriteHeader(200) _, _ = w.Write([]byte("OK")) @@ -53,48 +155,6 @@ func TestMealieImport(t *testing.T) { for range c { } - img, _ := uuid.Parse("f2f4b3aa-1e04-42a2-a581-607bf84f6800") - r := models.Recipe{ - Category: "uncategorized", - CreatedAt: time.Date(2024, 4, 12, 0, 0, 0, 0, time.UTC), - Description: "Roasted Vegetable Bowls! Crispy tender roasted veggies, buttery avocado, all together in a bowl with a drizzle of green tahini sauce.", - Images: []uuid.UUID{img}, - Ingredients: []string{ - "8 large carrots, peeled and chopped", "3 golden potatoes, chopped", - "1 head of broccoli, cut into florets", - "1 head of cauliflower, cut into florets", "olive oil and salt", - "1/2 cup olive oil (mild tasting)", "1/2 cup water", "1/4 cup tahini", - "a big bunch of cilantro and/or parsley", - "1 clove garlic", - "squeeze of half a lemon (about 2 tablespoons)", - "1/2 teaspoon salt (more to taste)", - "6 hard boiled eggs (or other protein)", - "3 avocados", - }, - Instructions: []string{ - "Prep", "Preheat the oven to 425 degrees.", "Roasted Vegetables", - "Arrange your vegetables onto a few baking sheets lined with parchment (I keep each vegetable in its own little section). Toss with olive oil and salt. Roast for 25-30 minutes.", "Sauce", - "While the veggies are roasting, blitz up your sauce in the food processor or blender.", - "Finish", - "Voila! Portion and save for the week! Serve with avocado or hard boiled eggs or… anything else that would make your lunch life amazing.", - }, - Keywords: []string{"Vegetable Bowl Recipe", "Roasted Vegetable Bowls", "Green Tahini"}, - Name: "Roasted Vegetable Bowls with Green Tahini", - Nutrition: models.Nutrition{ - Calories: "322 kcal", - Fiber: "6.7g", - Protein: "6.1g", - Sodium: "302.3g", - Sugars: "6.9g", - TotalCarbohydrates: "24.7g", - TotalFat: "24.6g", - }, - Times: models.Times{Prep: 15 * time.Minute, Cook: 30 * time.Minute}, - Tools: []models.HowToItem{}, - UpdatedAt: time.Date(2024, 04, 12, 0, 0, 0, 0, time.UTC), - URL: "https://pinchofyum.com/30-minute-meal-prep-roasted-vegetable-bowls-with-green-tahini", - Yield: 6, - } - want := models.Recipes{r, r, r} + want := models.Recipes{wantRecipe, wantRecipe, wantRecipe} assertRecipes(t, got, want, files) } diff --git a/internal/scraper/britishbakels.go b/internal/scraper/britishbakels.go index bbd153fa..3e9ee346 100644 --- a/internal/scraper/britishbakels.go +++ b/internal/scraper/britishbakels.go @@ -42,7 +42,7 @@ func scrapeBritishBakels(root *goquery.Document) (models.RecipeSchema, error) { if isLeft { numRight = 0 } else { - numRight += 1 + numRight++ } if numRight == 2 { diff --git a/internal/scraper/quitoque.go b/internal/scraper/quitoque.go index 71b33607..9086457d 100644 --- a/internal/scraper/quitoque.go +++ b/internal/scraper/quitoque.go @@ -18,7 +18,7 @@ func scrapeQuitoque(root *goquery.Document) (models.RecipeSchema, error) { productTagsNode := root.Find("#product-tags") var keywords []string - productTagsNode.Find("span").Each(func(i int, s *goquery.Selection) { + productTagsNode.Find("span").Each(func(_ int, s *goquery.Selection) { keywords = append(keywords, strings.TrimSpace(s.Text())) }) rs.Keywords.Values = strings.Join(keywords, ",") diff --git a/internal/services/files_apps.go b/internal/services/files_apps.go index 072c2dc2..1329fd86 100644 --- a/internal/services/files_apps.go +++ b/internal/services/files_apps.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/PuerkitoBio/goquery" "github.com/google/uuid" + "github.com/reaper47/recipya/internal/integrations" "github.com/reaper47/recipya/internal/models" "github.com/reaper47/recipya/internal/utils/extensions" "io" @@ -321,7 +322,14 @@ func (f *Files) extractJSONRecipes(rd io.Reader) (models.Recipes, error) { case '{': var rs models.RecipeSchema err = json.Unmarshal(buf, &rs) - xrs = append(xrs, rs) + + if rs.Ingredients == nil && rs.Instructions == nil { + var m integrations.MealieRecipe + err = json.Unmarshal(buf, &m) + xrs = append(xrs, m.Schema()) + } else { + xrs = append(xrs, rs) + } case '[': err = json.Unmarshal(buf, &xrs) default: diff --git a/web/components/recipes.templ b/web/components/recipes.templ index e8ac73a4..1af6c5a8 100644 --- a/web/components/recipes.templ +++ b/web/components/recipes.templ @@ -121,6 +121,7 @@ templ addRecipe() { plain text files or files that adhere to the recipe schema standard. + You may import all your Mealie or Tandoor recipes from the Data tab in the settings.

You can also download recipe schema files directly using the