Help: #howto Build Recipe "database" in Obsidian (complex)

Hi Matthias :grinning:

A (hopefully small) request/question:

Is there a way to autocalculate the ingredients per portion and make it populate the ingredients section in the YAML?

I would have to calculate that for thousands of recipes. Also, not every recipe states the amount of portions. I don’t know how to manage the ingredients in that situation. Estimate the amount of portions?

Quite time-consuming to calculate that by hand, where pasting in just the amounts mentioned in the recipe would be a no brainer.

portions: 4

ingredients:

  • [“sugar, white”, 20 g]
  • [watermelon, 286 g] # 1 wedge

The idea was to specify the ingredients per portion, and the recipe template already multiplies that by the number of portions you specify in the YAML. Try changing just the YAML portions variable and see what happens! (Closing/opening note might be required due to Dataview’s caching.)

Most recipes given in cookbooks here are for 4 portions. In this case, I just divide by 4 and enter that as the ingredient amount (sometimes ending up with something like 1.25 eggs, but what the heck).

You could of course go and modify the code to do the reverse, i.e. giving ingredients per x portions, and divide.

I understand the idea, and it’s working really well, of course I tried it after you told me the code can do that :slight_smile: :blush:

I wouldn’t know how to change the code, but I will try to see if I understand what the code actually says that it will do. I guess * means multiply, I remember that much from 45 years ago in Math class. And divide was / if I remember correctly.

1 Like

In any case, for later calculations, it makes sense to base things on always the same size (i.e., 100 g, 1 serving, etc.). Otherwise you’ll find that combined calculations, say combined GL or calories for a serving or complete recipe, will turn out almost impossible.

That’s the main reason I used “100 g” for GL and cal, and “1 portion” as a base for the recipes.

I know this can be a lot of work. Maybe if most of your recipes are already in Paprika, you can have that calculate the values and then copy to Obsidian?

Most of my recipes are still txt, docx or pdf based and not in Paprika. Paprika has about 100 or something.

I understand what you are saying about the 100g and 1 portion etc.

I just added the ingredients and the recipe for a Citroen MugCake. I followed the Melon Salad recipe and its ingredients precisely, I think I checked 100 times if it were indeed the same. It should be (and is!), as I ran the note off a template that is based on the Melon Salad recipe and that recipe and those ingredients work.

The only thing that is different, is that I didn’t use grams for the ingredients but tbsp and tsp. Because that is what is in the recipe. At first I added the Dutch equivalent of tbsp and tsp (el and tl) as units, but the code give a looooong error.

So I changed that in both the ingredients notes and the recipe note to tbsp and tsp. Same error.

I used things like 0,5 tbsp and thought, maybe it needs to be 0.5 tbsp. What do you know…it started working! <= these are the things that are not always obvious to me…why doesn’t it take 0,5 ?

After this, I could use my own units too. I have a pdf for you to see how I styled everything. Citroen MugCake

You can probably just drag-n-drop the PDF into the forum edit box.

Traditionally, programming languages are mostly using English terms, and also English number formats. That’s why I didn’t bother and just use them internally. For displaying, numbers could theoretically be converted to the number format of the language the system or Obsidian uses. But again I didn’t bother since it’s just an example to get started.

You could, for instance, in the recipe dataviewjs, replace the line

list.push(amount * dv.current().portions + " " + unit + " " + link);

against

list.push((amount * dv.current().portions).toLocaleString(moment.locale(), {maximumFractionDigits: 1}) + " " + unit + " " + link);

and it would magically output numbers (in the ingredients list) according to whatever language you’ve set Obsidian to:

Auswahl_021

EDIT: Unfortunately this is currently broken in my 0.12.4 installation.

Also, to make sense of all the units and get nutritional values out of them, conversion functions need to be written. Nothing is “automatic”, and a computer will blindly do what it is told. So for things like “piece”, “tsp” or “tbsp”, it would need to know if it’s a fluid or mass measurement, and the conversion values. A rather complicated undertaking.

Looks quite nice already, your mug cake PDF!

Btw, looks like you use an older version of the recipe template, where the ingredients have a large line spacing. I modified that later (somewhere in this thread) to use less line spacing.

1 Like

The ine spacing thing was indeed one of the things I wanted to ask. Obsidian sometimes makes them big too, but then when I close and re-open the note, it’s okay again.

It’s not so much a bother, the dot or the comma in the numbers. It was just that I discovered that this was the mistake I made to break it all (seemingly) :slight_smile: but, I will try to see if I can implement what you wrote above and make them appear as a euopean notation with a dot for thousands and up and a comma for below and for not-whole numbers.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.

I know this originally started as a help thread, but I think the contents can continue to be discussed and/or other users might want to contribute/ask/share their own versions, so I’m moving this to #share-showcase and reopening the thread.

Cheers!

5 Likes

Thanks! I want to investigate this myself once I get some less important stuff out of the way.

1 Like

Hah, great thread this. Both entertaining and educating read. Thanks to all involved! :smiley:

1 Like

Also my thanks for these :slight_smile: its a cool idea
@Moonbase59 or @FiekeB can someone of you help me?
i only have two problems at the moment, i copied your zip into a new vault and wanted to try it out, you already have the melon salat recipe in your more or less template. but the recipe dont get shown in both TOCs, i also have installed the dataview addon and enabled inside it the javascript queries and the inline queries. but its still dont work, can you help me? It says following: “Dataview: Query returned 0 results” for reciepes and the TOC for the incredients didnt give any message out its just the title.
FYI im using obsidian 0.12.15

and the second is that i cant find the line.push… code in any dataview script block from your post 40 to change it (even when i didnt understood completly what it should do :smiley:

or did you already have since than even more awesome ideas for the reciepbook inside obsidian and want to share it with the community :slight_smile:

okay, i found out the problem, you have in the zip file no folder with the name “recipe” but you search in both TOC searches inside the folder “recipe” AND for a tag. but the folder eg for the food reciepes is “food” and not reciepe :smiley:

the second of my question: i seen that, in the message 19 from Moonbase59 (where the zip is attached) he have written in the recipe template

list.push(amount * dv.current().portions + " " + unit + " " + link

and in the zip is standing

dv.paragraph("- " + amount * dv.current().portions + " " + unit + " " + link

but i still dont understand the difference between them :frowning:

ADVANCED
Hi all
I have fiddled a little at a recipe repository and have gotten inspired by this thread (and the recipe app i am otherwise using and will delete once my recipes are all imported into Obsidian); so thanks @FiekeB for the idea and @Moonbase59 for some of the implementation ideas/considerations.
I just wanted to lay out the solution that i have implemented as a thank you and hopefully this can be helpful. The solution, however, require a basic understanding of JavaScript. I’ll try to be as explicit as possible tho.
Hopefully this can be helpful for some users - and can be declined for other types of library management (books, movies, games…)!

Requirements
dataview/customJS/MetaEdit/Buttons

Structure

  • 1 note per recipe
  • 1 (or several) aggregation note
    (no ingredient note…)

For my own purpose, I have tried to be as flexible as possible to accommodate different recipe writing styles and sources.
I will break down this post in different posts: the recipe page; the aggregation page; backend

2 Likes

Recipe note

Frontmatter:

---
ServingSize:
cssclass: recipeTable
Alias: []
Tag: []
Date: 2021-09-21
DocType: "Recipe"
CollapseMetaTable: Yes
Meta:
 IsFavourite:
 Rating:
Recipe:
 Courses:
 Categories:
 Collections:
 Source:
 PreparationTime: 
 CookingTime:
 OServingSize:
Ingredients:
 - 
---

I think that the headers are quite self explanatory. I have 2 fields ServingSize and OServingSize: OServingSize represents the original number of portions that the recipe is based on; ServingSize is a variable to adjust ingredient amounts for own purpose. It allows not to have to manually adjust ingredient amounts when importing a recipe.
It is important to keep ServingSize at the top of the FrontMatter but i’ll explain below.

In the ingredient section, input follows this nomenclature: <amount> <unit> <free text for ingredient>, enabling to have more than a word describing an ingredient.

Edit Serving Size
I have created a button using the Buttons & MetaEdit plugins in order to easily update the FrontMatter, which mostly serves to update the ServingSize for ease of manipulation and not have to go back into edit the actual note each time one would like to change the portion size of their dish.
By keeping the field ‘ServingSize’ at the top of the frontmatter, you do not have to scroll down the list of fields when clicking the button, and have an immediate access to this field! It is a question of personal taste, tho!

/```button
name Edit Recipe parameters
type command
action MetaEdit: Run MetaEdit
id EditMetaData
/```

Generic information section
I have a generic information section populated from the ‘Recipe’ part of the FrontMatter:

| |
|-|-
**Courses**: | `$=dv.current().Recipe.Courses`
**Categories**: | `$=dv.current().Recipe.Categories`
**Collections**: | `$=dv.current().Recipe.Collections`
**Serving size**: | `$=dv.current().ServingSize`
**Cooking time**: | `$=dv.current().Recipe.CookingTime` min

which looks like the below:

Ingredients section
In this section, I have used a JS script, using dv.view() from dataview that allows to write 1 code snippet only and reuse it in different notes/sections. On each page, i only reference the code which is easier to maintain. The actual code snippet is going to be shown in the ‘Backend’ section.

On the note, the code looks like:

/```dataviewjs
dv.view("path/to/dv-views/query_ingredient", {ingredients: dv.current().Ingredients, originalportioncount: dv.current().Recipe.OServingSize})
/```

which renders:

Instructions/Direction section
Manual update for each recipe

Backend for the recipe note

In order to use dv.view() from dataview, you need to set up a folder from within Obsidian in which to store these JS snippet and have a JS editor (ex: Textatic for iOS) in order to generate and write into those files.

Once you have created the js file in the JS editor (query_ingredient in my case), you can then copy/paste the below:

let {ingredients, originalportioncount} = input;
let ing = [];

if (!Boolean(originalportioncount)) {
	return "⚠️ <b>Warning</b>\nYou have not specified the original serving size!\n<b>Ingredient amounts cannot be estimated.</b>"
}

for (let i = 0; i < ingredients.length; i++) {
let name = ingredients[i].replace(ingredients[i].split(" ")[0] + " " + ingredients[i].split(" ")[1], "");
let unit = ingredients[i].split(" ")[1];
let amount = ingredients[i].split(" ")[0];
ing[i] = "🥣 " + (amount / originalportioncount * dv.current().ServingSize) + " " + unit + " <b>" + name + "</b>"
}
dv.list(ing)

Note that the code reference 2 arguments: the Ingredient list and the OServingSize FrontMatter filed to rebase the ingredients amounts.

4 Likes

Aggregation view

Given the fact that a recipe repository can become quite big over time, I have written a code snippet that allows for searching a recipe based on different criteria in the frontmatter (on top of normal/static views that can be generated through a normal dataview block) rather than browse through hundreds of files. I’ll try to detail all steps of this search function that can be adapted to other types of libraries.

Note frontmatter
In the note frontmatter, i have placed a series of query terms:

---
QueryCourse:
QueryCategory:
QueryCuisine:
QueryIngredient:
QueryTheme:
QueryFavourite:
QueryRating:
---

One can search recipes by:

  • course
  • Category
  • Collection/cuisine type
  • ingredient
  • theme (tag)
  • whether the recipe is marked favourite (true/false argument)
  • rating

Each term can accept one or several values but for the time being, it performs only a match (no greater/lower than…)!

I have then placed a button that allows to edit those parameters without going into the frontmatter:

/```button
name Search Library
type command
action MetaEdit: Run MetaEdit
id EditMetaData
/```

A textbox that allows me to see what query terms i have entered:

/```dataviewjs
dv.view("/path/to/dv-views/print_data", {toprint: [dv.current().QueryCourse, dv.current().QueryCategory, dv.current().QueryCuisine, dv.current().QueryIngredient, dv.current().QueryTheme, dv.current().QueryFavourite, dv.current().QueryRating,
dv.current().QueryAddedDate]})
/```

The textbox calls a JS snippet called ‘print_data’ that i will detail in the backend section thereafter.

And the actual search query:

/```dataviewjs
dv.view("/path/to/dv-views/query_recipe", {course: dv.current().QueryCourse, dateadded: dv.current().QueryAddedDate, category: dv.current().QueryCategory, cuisine: dv.current().QueryCuisine, ingredient: dv.current().QueryIngredient, theme: dv.current().QueryTheme, isfavourite: dv.current().QueryFavourite, rating: dv.current().QueryRating})
/```

This section calls a JS snippet called ‘query_recipe’ that i will detail in the backend section thereafter.

which all renders like the below:

Backend section

Print_data

This is a JS snippet file to be generated (or pasted directly into the note):

let {toprint} = input;

function BuildList(arg1, construct) {
		
		let ilength = arg1.length
		let TempS = ""
	
		for (let i = 0; i < ilength; i++) {
			if (Boolean(arg1[i])) {
				if (TempS == "") {
					TempS = arg1[i]
				} else {
					TempS = TempS + construct + arg1[i]
				}
			}
		}
		return TempS
	}

dv.el('b', "🔎 search terms: \n• " + BuildList(toprint, "\n• "));

Query_recipe

This particular section requires the CustomJS plugin. There are two parts to this: the query called from the note and the engine solving the search.
The query called from the note is to be placed in a stand-alone JS file (called query_recipe in my case):

const {recipeFunc} = customJS
const DataType = 'Recipe'
let {course, category, cuisine, ingredient, dateadded, theme, isfavourite, rating} = input;
const iArray = [course, category, cuisine, ingredient, dateadded, theme, isfavourite, rating];
const dArray = ["course", "category", "cuisine", "ingredient", "dateadded", "theme", "isfavourite", "rating"];

return recipeFunc.getTable(dv, DataType, dArray, iArray, 0)

Note that the code references ‘CustomJS’ as mentioned above, to solve the query and print a table with the results.
CustomJS needs to be set but all is explained clearly on the plugin page.

To generate the CustomJS code, just create a new JS file with the below code and the search function should work off the cuff.

class recipeFunc {
	DataCheck(arg1, arg2) {
		
		var iarg1 = arg1
		if (moment(iarg1).isValid()) {iarg1 = arg1.toString()}
		var iarg2 = arg2
		if (moment(iarg2).isValid()) {iarg2 = arg2.toString()}
		
		if (!Array.isArray(iarg2) && !Array.isArray(iarg1)) {
			var resultdc = iarg1.search(new RegExp(iarg2, "i")) > -1
		} else if (!Array.isArray(iarg1)) {
			let tempresult = false
			for (let i = 0; i < iarg2.length; i++) {
				tempresult = tempresult || iarg1.search(new RegExp(iarg2[i], "i")) > -1
			}
			var resultdc = tempresult
		} else if (!Array.isArray(iarg2)) {
			let tempresult = false
			for (let i = 0; i < iarg1.length; i++) {
				tempresult = tempresult || iarg1[i].search(new RegExp(iarg2, "i")) > -1
			}
			var resultdc = tempresult
		} else {
			let tempresult = false
			for (let i = 0; i < arg2.length; i++) {
				for (let j = 0; j < arg1.length; j++) {
					tempresult = tempresult || iarg1[j].search(new RegExp(iarg2[i], "i")) > -1
				}
			}
			
			var resultdc = tempresult
		}
		
		return resultdc
	}
	
	Get1stArg(arg3) {
		if (!Array.isArray(arg3)) {
			return arg3
		} else {
			return arg3[0]
		}
	}
	
	topLevelFilter(pobj, DocType, subType) {
		let result = true
		let folderExcl = (DocType == 'Source') ? '00.01' : '00.';
		
		result = !pobj.file.path.contains(folderExcl) && this.GetPoint(pobj, "main", "type") !== undefined && this.GetPoint(pobj, "main", "type") !== null && this.GetPoint(pobj, "main", "type").contains(DocType)
		
		return result
		
	}
	
	IsInSearch(pobj, DocType, darray, iarray) {
	
		let ilength = iarray.length;
		let result = true
	
		for (let i = 0; i < iarray.length; i++) {
			
			if (iarray[i] == undefined || darray[i] == undefined) {result = result && true} else {
					if (Boolean(iarray[i])) {
						let pProp = this.GetpProp(pobj, DocType, darray[i])
						if (!Boolean(pProp)) { result = result && false } else {
							result = result && this.DataCheck(pProp, iarray[i])
						}
					} else {result = result && true}
			}
		}
		return result
	}
	
	getTable(dv, DocType, darray, iarray, tabletype) {
		
//		let tablet = (darray.contains('tabletype')) ? iarray[0] : 0;
		
		let page = dv.pages()
		.filter(p => p && this.topLevelFilter(p, DocType, 0))
		.where(p => p && this.IsInSearch(p, DocType, darray, iarray))

		if (page.length === 0) {
			return this.EmptyQueryMessage()
		}

	dv.table(this.GetTableHeaders(DocType, tabletype), page
			.sort(p => p.file.name, `asc`)
			.map(p => this.GetTableMap(DocType, tabletype,p))); 
	}
	
	EmptyQueryMessage() {
		return dv.el('b', '⚠️ Warning:\nNo result matching your query!')
	}
	
	GetTableHeaders(DataT, TableT) {
		
		let TempData = ["Name"]
		
		switch(DataT) {
			
			case 'Recipe':
			
				TempData = ["Name", "Main ingredients", "Cuisine", "Cooking time", "Rating (1-5)"]
			
			break;
			
		}
	
		return TempData
	
	}
	
	GetTableMap(DataT, TableT, p) {
	
		let TempData = [p.file.link]
	
		switch(DataT) {
			
			case 'Recipe':
			
				TempData = [p.file.link, this.GetPoint(p, DataT, "category"), this.GetPoint(p, DataT, "collection"), this.GetPoint(p, DataT, "cooking") + " min", this.GetPoint(p, DataT, "rating")]
			
			break;
		
		}
	
		return TempData
		
	}
	
	GetpProp(pobj, DocType, dPoint) {

		let result = null

		switch(dPoint) {
			case 'dateadded':
				result = this.GetPoint(pobj, "main", "date")
			break;
			case 'theme':
				result = this.GetPoint(pobj, "main", "tag")
			break;
			case 'course':
				result = this.GetPoint(pobj, DocType, "course")
			break;
			case 'category':
				result = this.GetPoint(pobj, DocType, "category")
			break;
			case 'cuisine':
				result = this.GetPoint(pobj, DocType, "collection")
			break;
			case 'ingredient':
				result = this.GetPoint(pobj, DocType, "ingredient")
			break;
			case 'isfavourite':
				result = this.GetPoint(pobj, DocType, "isfavourite")
			break;
			case 'rating':
				result = this.GetPoint(pobj, DocType, "rating")
			break;
		}
		return result
	}
	
	GetPoint(pobj, fstline, dPoint) {
	
		let result = null
	
		switch(fstline) {
		
			case 'main':
			
				switch(dPoint) {
					case 'alias':
						result = pobj.Alias
					break;
					case 'tag':
						result = pobj.Tag
					break;
					case 'date':
						result = pobj.Date
					break;
					case 'type':
						result = pobj.DocType
					break;
					case 'meatatable':
						result = pobj.CollapseMetaTable
					break;
				}
			
			break;
			
			case 'Recipe':
				
			switch(dPoint) {
				case 'course':
					result = pobj.Recipe.Courses
				break;
				case 'category':
					result = pobj.Recipe.Categories
				break;			
				case 'collection':
					result = pobj.Recipe.Collections
				break;
				case 'source':
					result = pobj.Recipe.Source
				break;
				case 'preparation':
					result = pobj.Recipe.PreparationTime
				break;
				case 'cooking':
					result = pobj.Recipe.CookingTime
				break;
				case 'ingredient':
					result = pobj.Ingredients
				break;
				case 'isfavourite':
					result = pobj.Meta.IsFavourite
				break;
				case 'rating':
					result = pobj.Meta.Rating
				break;
			}
			
			break;

		}
		return result
	}
}

5 Likes

Nice thread, I have a bunch (650 or so) of recipes in Paprika and some interest in getting Obsidian to work as a recipe manager. Paprika is a very decent app, I have to say, but I don’t like being locked in as others have echoed.

a) sync or import from Paprika app or files. The import should be relatively trivial as the data is already quite well organised into JSON format. The export file is simply a zipped up JSON. The chap who wrote Tandoor has a bunch of Python scripts written for importing recipes into his DB, I’d assume that’s a lot of the work done already to get something working. Whether that would be a plugin or Python standalone script I don’t really know.
b) web clipping of recipes. I haven’t looked into it much, but there is an open standard (schema.org/recipe) for recipes seemingly on the web. Have a look at the source code to this Food Wishes recipe for example, it’s all JSON data, quite ripe for converting into YAML data I’d have thought.

Have a big project I’m finishing up at the moment but I’ll come back to this at some point, and keep an eye on this thread.

1 Like

I noticed in Obsidian Roundup about a conversation in Reddit on CookLang , which seems to be like an ideal solution. Do we know if there is any Obsidian implementation?

2 Likes

FYI folks, I’m writing a Python script to unzip the .paprikarecipes files and write the contents to a markdown template.

My goals are:

  1. Move my recipes into Obsidian complete with tags, photos and the ability to scale them.
  2. Build a bookmarklet to scrape recipes from websites into markdown frontmatter - and load them into template files.

I’m 80% of the way through option 1, and will be happy to make the script public when I’m done.

The script processes the images in the paprika json and saves them to a specified folder.

I’ve managed to figure out how to display the images and the recipe steps using dataviewjs (thanks to @Moonbase59’s code here).

What I’m missing is the method of processing the ingredients. It’ll likely be the following format:

ingredients:
  - [1, tsp, salt]
  - [2, grams, pepper]

This will make it easy enough to process servings - you just make the ingredient quantity the size of a single portion, and the display quantity set to servings * quantity.

I’m looking into being able to extract the data in the format above from the paprika files. Currently I’ve saved each line in a list. There’s a few Python libraries about that kind of do this, I may end up just having a raw data column in the ingredient if the parsing doesn’t work very well for manual processing. This unfortunately means that migrating recipes would be a one time action. There is a decent natural language processor API but there’s a limit of 30 queries/day. It is on GitHub, but not so simple to set up for regular folks to clone a GitHub repo and get it running. There is a project here that looks promising.

I used a dictionary to convert some of the categories into nested tags in Obsidian. Paprika just outputs a single tag. That was a bit of a pain as I’ve got a lot of them, but it’s a one time thing for anybody who wants to set it up.

---
name: Yakisoba Noodles
source: Takashi's Noodles
ingredients: ['2 (3-ounce) pieces dried ramen noodles', '2 tablespoons finely shaved katsuobushi (dried bonito flakes)']
source: Takashi's Noodles
difficulty: 
photo_thumbnail: _resources/F0E6DF32-B823-4CC5-852F-F817CF17D15E.jpg
image_url: None
total_time: 
notes: []
nutritional_info: 
description: Yakisoba is an extremely popular casual dish in Japan, especially with kids. During the country’s annual summer festivals you can always find yakisoba
rating: 0
prep_time: 
created: 2020-10-25 18:12:49
directions: ['Place a large pot of water over high heat and bring to a boil. Add the noodles and cook, following package instructions. Rinse under cold running water. Once chilled, drain well and set aside.']
categories: ['Japanese', 'Noodles', 'Ramen']
source_url: 
cook_time: 
servings: [4]
tags: 
  - recipes/world/asian/japanese
  - recipes/noodles
  - recipes/soup/ramen
photos: 
  - _resources/F0E6DF32-B823-4CC5-852F-F817CF17D15E.jpg
  - _resources/F0E6DF32-B823-4CC5-852F-F817CF17D199.jpg
---
3 Likes