Automatically pick recipes from your Tandoor instance, create meal plans, and print beautiful weekly menus -- all from a single command.
Tandoor Menu Generator connects to your Tandoor Recipes server, selects recipes based on rules you define (like "at least 2 vegetarian" or "nothing cooked in the last 30 days"), and optionally creates meal plans and generates printable menu files. It uses constraint solving under the hood, but all you need to do is describe what you want in a config file.
- Quick Start
- Features
- Installation
- Configuration Guide
- Usage Examples
- Understanding Rules (Constraints)
- Meal Plan Integration
- Menu File Generation
- Command-Line Reference
- Troubleshooting and FAQ
- Contributing
- License
1. Clone and install dependencies:
git clone https://github.com/smilerz/tandoor-menu-generator.git
cd tandoor-menu-generator
pip install -r requirements.txt2. Create your config file:
cp config.ini.example config.iniOpen config.ini in a text editor and fill in your Tandoor URL and API token:
[create-menu]
url: https://your-tandoor-server.com
token: tda_your_api_token_hereWhere to find your API token: In Tandoor, go to Settings (gear icon) then scroll to API Tokens. Click Create to generate a new token. Copy the full token string (it starts with
tda_).
3. Run it:
python create_menu.pyYou should see output like this:
###########################
Your selected recipes are:
Recipe: <42> Chicken Parmesan: https://your-tandoor-server.com/view/recipe/42
Recipe: <17> Beef Tacos: https://your-tandoor-server.com/view/recipe/17
Recipe: <88> Caesar Salad: https://your-tandoor-server.com/view/recipe/88
Recipe: <23> Mushroom Risotto: https://your-tandoor-server.com/view/recipe/23
Recipe: <56> Grilled Salmon: https://your-tandoor-server.com/view/recipe/56
###########################
That's it -- you have a randomly selected menu of 5 recipes. Read on to add rules, create meal plans, or generate printable menus.
- Recipe selection with rules -- Define rules like "at least 2 with keyword Vegetarian" or "no more than 1 pasta dish." The tool finds a set of recipes that satisfies all your rules at once.
- Flexible recipe filtering -- Narrow the pool of recipes to choose from using search parameters, custom filters, or existing meal plans.
- Hierarchical keywords and foods -- When you specify a keyword like "Protein," child keywords (Chicken, Beef, Pork, etc.) are included automatically.
- Meal plan creation -- Automatically create meal plan entries in Tandoor for your selected recipes.
- Stale plan cleanup -- Remove old, uncooked meal plans before generating new ones.
- Printable menu files -- Generate PNG, JPG, GIF, or PDF menu files from SVG templates with recipe names and ingredients.
- API caching -- Results from Tandoor are cached locally to speed up repeated runs.
- Config file and CLI -- Set your preferences once in a config file, or override any setting from the command line.
- Python 3.9 or newer
- A running Tandoor Recipes instance
- An API token from your Tandoor instance (see How to Get Your API Token)
git clone https://github.com/smilerz/tandoor-menu-generator.git
cd tandoor-menu-generator
pip install -r requirements.txtThis installs everything needed for recipe selection and meal plan creation.
If you want to generate printable menu files (PNG, PDF, etc.) from SVG templates, you need the libcairo2 graphics library and some additional Python packages.
Debian / Ubuntu:
sudo apt install libcairo2-dev
pip install -r pdf_requirements.txtmacOS (Homebrew):
brew install cairo
pip install -r pdf_requirements.txt- Log in to your Tandoor instance.
- Click the gear icon (Settings) in the top navigation bar.
- Scroll down to the API Tokens section.
- Click Create to generate a new token.
- Copy the full token (starts with
tda_) and paste it into yourconfig.ini.
Security note: Your API token grants full access to your Tandoor account. Do not share it or commit it to version control. The
config.inifile is listed in.gitignoreby default.
Tandoor Menu Generator uses a config file (config.ini) as its primary interface. Every setting in the config file can also be overridden from the command line. When both are provided, the command-line value takes priority.
The config file uses INI format with four sections: [create-menu], [recipes], [conditions], and [mealplan]. Menu file settings go in the [menufile] section.
The bare minimum to get started -- this picks 5 random recipes from all your recipes:
[create-menu]
url: https://your-tandoor-server.com
token: tda_your_api_token_here
[conditions]
choices: 5Many settings require the numeric ID of a keyword, food, book, meal type, or user. To find an ID:
- Navigate to the item in Tandoor (e.g., open a keyword, food, or meal type).
- Look at the URL bar in your browser -- the number at the end is the ID.
- Example:
https://tandoor.example.com/list/keyword/47/-- the keyword ID is47.
- Example:
| Config key | Default | Description |
|---|---|---|
url |
(required) | Full URL of your Tandoor server, including protocol and port. Example: https://tandoor.example.com:8080 |
token |
(required) | Your Tandoor API token (starts with tda_). |
log |
info |
Logging level. Set to debug for verbose output. |
cache |
240 |
Minutes to cache API results. Set to 0 to disable caching. |
These settings control which recipes are eligible to be chosen. By default, all recipes are considered. Use one or more of these to narrow the pool.
| Config key | Section | Default | Description |
|---|---|---|---|
recipes |
[recipes] |
(all recipes) | JSON object of search parameters to filter recipes. See Tandoor API docs for available parameters. |
filter |
[recipes] |
[] |
List of CustomFilter IDs. Recipes matching any of these filters are included. |
plan_type |
[recipes] |
[] |
List of MealType IDs. Recipes from meal plans of these types on mp_date are included. |
These settings define rules about the recipes that get selected. See Understanding Rules for full documentation and examples.
| Config key | Section | Default | Description |
|---|---|---|---|
choices |
[conditions] |
5 |
Number of recipes to select. |
keyword |
[conditions] |
[] |
Rules based on recipe keywords. |
food |
[conditions] |
[] |
Rules based on recipe ingredients (foods). |
book |
[conditions] |
[] |
Rules based on recipe books. |
rating |
[conditions] |
[] |
Rules based on recipe rating. |
cookedon |
[conditions] |
[] |
Rules based on when a recipe was last cooked. |
createdon |
[conditions] |
[] |
Rules based on when a recipe was created. |
include_children |
(CLI only) | true |
When enabled, child keywords and foods satisfy parent rules (e.g., a rule for "Protein" also matches "Chicken"). |
| Config key | Section | Default | Description |
|---|---|---|---|
create_mp |
[mealplan] |
false |
Enable meal plan creation. |
mp_type |
[mealplan] |
(required when create_mp is true) |
ID of the MealType to use for created plans. |
mp_date |
[create-menu] |
0days |
Date for new meal plan entries. Accepts YYYY-MM-DD or Xdays (X days from today). |
mp_note |
[mealplan] |
Created by: Tandoor Menu Generator. |
Note text added to each meal plan entry. |
share_with |
[mealplan] |
[] |
List of user IDs to share the meal plan with. |
cleanup_mp |
[mealplan] |
false |
Delete uncooked meal plans from previous runs before creating new ones. |
cleanup_date |
[mealplan] |
-7days |
Starting date for cleanup. Plans from this date onward (of the same meal type) that were not cooked are deleted. Accepts YYYY-MM-DD or -Xdays. |
| Config key | Section | Default | Description |
|---|---|---|---|
create_file |
[menufile] |
false |
Enable menu file generation from an SVG template. |
file_template |
[menufile] |
(required when create_file is true) |
Name of the SVG template file, located in the templates/ directory. |
file_format |
[menufile] |
PNG |
Output format: GIF, JPG, PNG, or PDF. |
output_dir |
[menufile] |
templates/ |
Directory for the output file. Defaults to the templates directory. |
fonts |
[menufile] |
[] |
Custom fonts needed by the SVG template. Format: [{"name": "FontName", "file": "font.ttf"}]. Font files must be in the templates/ directory. |
replace_text |
[menufile] |
(none) | Defines how template placeholder text maps to recipe data. See Menu File Generation. |
separator |
[menufile] |
' - ' |
Text used to join ingredients on a single line (e.g., Chicken - Rice - Peppers). |
The default behavior with a minimal config:
[create-menu]
url: https://tandoor.example.com
token: tda_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[conditions]
choices: 5python create_menu.pyAssuming your "Vegetarian" keyword has ID 73:
[create-menu]
url: https://tandoor.example.com
token: tda_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[conditions]
choices: 7
keyword: [{"condition": 73, "count": 2, "operator": ">="}]Assuming "Chicken" food ID is 15 and "Pasta" keyword ID is 47:
[conditions]
choices: 5
food: [{"condition": 15, "count": 2, "operator": ">="}]
keyword: [{"condition": 47, "count": 1, "operator": "<="}][conditions]
choices: 5
cookedon: [{"condition": "30days", "count": 0, "operator": "=="}]This says: "Of all recipes cooked in the last 30 days, select exactly 0." In other words, none of the selected recipes should have been cooked recently.
Select 5 recipes, all with a rating of 4 or higher:
[conditions]
choices: 5
rating: [{"condition": 4, "count": 5, "operator": ">="}]This means: "At least 5 of the selected recipes must have a rating of 4 or above." Since you are selecting 5 total, this ensures all of them are highly rated.
[create-menu]
url: https://tandoor.example.com
token: tda_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
mp_date: 0days
[conditions]
choices: 5
[mealplan]
create_mp: true
mp_type: 3
share_with: [2, 5]
mp_note: This week's menu[menufile]
create_file: true
file_template: weekly_menu.svg
file_format: PNGSee Menu File Generation for details on setting up templates.
[mealplan]
create_mp: true
mp_type: 3
cleanup_mp: true
cleanup_date: -7daysThis deletes any uncooked meal plans of type 3 from the last 7 days before creating new ones.
Any config setting can be overridden on the command line:
# Pick 7 recipes instead of the 5 in your config
python create_menu.py --choices 7
# Use a different config file
python create_menu.py -c my_other_config.ini
# Enable debug logging for this run
python create_menu.py --log debugRules (called "constraints" internally) let you control which recipes are selected. You can set rules based on keywords, foods, books, ratings, and dates.
Each rule is a JSON object with three required fields:
{"condition": <value>, "count": <number>, "operator": "<comparison>"}| Field | Description |
|---|---|
condition |
What to match: a keyword ID, food ID, book ID, rating number, or date string. Can be a single value or a list of IDs. |
count |
How many of the selected recipes should match this condition. |
operator |
How to compare: >= (at least), <= (at most), or == (exactly). |
| Operator | Meaning | Example |
|---|---|---|
>= |
At least | "At least 2 vegetarian recipes" |
<= |
At most | "At most 1 pasta dish" |
== |
Exactly | "Exactly 3 recipes from Book X" |
Rules can include additional fields to refine the match:
| Modifier | Type | Description |
|---|---|---|
exclude |
true/false |
When true, the rule applies to all recipes that do not match the condition. For example, "exclude": true with a "Vegetarian" keyword means the rule applies to non-vegetarian recipes. |
except |
ID or list of IDs | Excludes specific items from a hierarchical condition. For example, a rule for "Protein" (which includes Chicken, Beef, Pork) with "except": [45] would match all proteins except the one with ID 45. |
cooked |
date string | Further filters matching recipes to only those cooked on or after/before a date. Accepts YYYY-MM-DD or Xdays. |
created |
date string | Further filters matching recipes to only those created on or after/before a date. Accepts YYYY-MM-DD or Xdays. |
Dates can be specified in two ways:
| Format | Meaning | Example |
|---|---|---|
YYYY-MM-DD |
Absolute date. Matches recipes on or after this date. Prefix with - to match on or before. |
2024-01-01 (on or after), -2024-06-01 (on or before) |
Xdays |
Relative date. X days ago from today. Prefix with - to mean "on or before X days ago." |
30days (within the last 30 days), -30days (more than 30 days ago) |
"At least 2 vegetarian recipes" (Vegetarian keyword ID: 73):
{"condition": 73, "count": 2, "operator": ">="}"At most 1 recipe with pasta" (Pasta keyword ID: 47):
{"condition": 47, "count": 1, "operator": "<="}"Exactly 3 recipes from my Favorites book" (Book ID: 5):
{"condition": 5, "count": 3, "operator": "=="}"At least 1 recipe with any protein except chicken" (Protein keyword ID: 100, Chicken keyword ID: 99):
{"condition": 100, "count": 1, "operator": ">=", "except": [99]}"At least 2 recipes with either Italian or Mexican keywords" (Italian ID: 73, Mexican ID: 273):
{"condition": [73, 273], "count": 2, "operator": ">="}"No recipes cooked in the last 14 days":
{"condition": "14days", "count": 0, "operator": "=="}You can specify multiple rules for the same type. They are all applied simultaneously:
keyword: [
{"condition": 73, "count": 2, "operator": ">="},
{"condition": 47, "count": 1, "operator": "<="}
]This says: "At least 2 Vegetarian recipes AND at most 1 Pasta recipe."
You can also combine rules of different types:
keyword: [{"condition": 73, "count": 2, "operator": ">="}]
food: [{"condition": 15, "count": 1, "operator": ">="}]
rating: [{"condition": 4, "count": 3, "operator": ">="}]This says: "At least 2 Vegetarian, at least 1 with Chicken, and at least 3 rated 4 or above."
If your rules are impossible to satisfy (e.g., "at least 4 vegetarian out of 5" combined with "at least 3 with beef"), the solver will report "No solution found." When this happens:
- Relax one or more rules (lower the count, change
>=to<=, etc.) - Increase the number of
choicesto give the solver more room - Run with
--log debugto see which constraints were applied and how many recipes matched each one
Ratings use a scale from 0 to 5. A negative condition value means "less than or equal to" the absolute value. For example:
"condition": 4-- matches recipes rated 4 or above"condition": -3-- matches recipes rated between 0 and 3 (inclusive)
To have the tool create meal plan entries in Tandoor, add a [mealplan] section to your config:
[create-menu]
mp_date: 0days
[mealplan]
create_mp: true
mp_type: 3mp_typeis the ID of the MealType to use. You can find this in your Tandoor URL bar when viewing meal types.mp_datesets the date for the new meal plan entries.
Tip: Create a dedicated MealType in Tandoor (like "Generated Menu") for plans created by this tool. This keeps them separate from manually-created plans and makes cleanup reliable.
The mp_date setting accepts two formats:
| Format | Meaning | Example |
|---|---|---|
Xdays |
X days from today | 0days (today), 1days (tomorrow), 7days (next week) |
YYYY-MM-DD |
A specific date | 2024-12-25 |
To share the generated meal plans with other Tandoor users, add their user IDs:
[mealplan]
create_mp: true
mp_type: 3
share_with: [2, 5]If you run the tool regularly (e.g., weekly), you can have it delete old uncooked plans before creating new ones:
[mealplan]
create_mp: true
mp_type: 3
cleanup_mp: true
cleanup_date: -7daysThis looks at all meal plans of the specified type from 7 days ago onward. Any plans whose recipes were not marked as cooked in Tandoor get deleted. Plans for recipes that were cooked are preserved.
Menu file generation creates a printable image or PDF from an SVG template by replacing placeholder text with recipe names and ingredients.
Make sure you have installed the menu file dependencies (libcairo2 and pdf_requirements.txt).
[menufile]
create_file: true
file_template: my_menu.svg
file_format: PNGPlace your SVG template file in the templates/ directory within the project folder.
The tool opens your SVG template, finds specific placeholder text strings, and replaces them with actual recipe data. You define the mapping between placeholder text and recipe data in the replace_text config option.
The placeholder text in your SVG must match exactly (including case and spacing). The replacement text is truncated to fit within the length of the placeholder, so use long placeholder strings to allow room for recipe names and ingredients.
The replace_text option is a YAML/JSON object with two main sections:
replace_text: {
date_text: {
'date': 'WEEK OF PLACEHOLDER',
'ordinal': 'XY',
'format': 'short'
},
recipe_text: [
{
'name': 'Recipe Title One Placeholder Text Here',
'ingredients': [
'Ingredient Line 1 placeholder text goes here',
'Ingredient Line 2 placeholder text goes here',
'Ingredient Line 3 placeholder text goes here'
]
},
{
'name': 'Recipe Title Two Placeholder Text Here',
'ingredients': [
'Ingredient Line 1 placeholder text goes here',
'Ingredient Line 2 placeholder text goes here'
]
}
]
}date_text (optional) -- Replaces date placeholder text with the meal plan date:
| Key | Description |
|---|---|
date |
Placeholder text to replace with the formatted date. |
ordinal |
Placeholder text to replace with the day's ordinal suffix (st, nd, rd, th). |
format |
Date format: short (Jan 15), medium (January 15), long (January 15, 2024), or number (01/15). |
recipe_text (required) -- A list of objects, one per recipe. Each object defines:
| Key | Description |
|---|---|
name |
Placeholder text in the SVG to replace with the recipe name. |
ingredients |
(Optional) List of placeholder text strings for ingredient lines. Ingredients are concatenated using the separator setting and wrapped across lines automatically. |
The number of entries in recipe_text should match your choices setting.
The separator setting controls how ingredients are joined on each line. The default is ' - ', which produces output like:
Chicken - Rice - Bell Peppers - Onions
| Format | Output example |
|---|---|
short |
Jan 15 |
medium |
January 15 |
long |
January 15, 2024 |
number |
01/15 |
If your SVG template uses non-system fonts, register them in the config:
[menufile]
fonts: [{"name": "MyCustomFont", "file": "MyCustomFont.ttf"}]Place the .ttf font files in the templates/ directory alongside your SVG template.
| Format | Best for |
|---|---|
PNG |
Sharing on screens, messaging apps, photo frames |
JPG |
Smaller file size, photos |
GIF |
Older systems |
PDF |
Printing, archiving |
- Use a vector graphics editor like Inkscape (free) to create your SVG template.
- Make placeholder text strings longer than the longest recipe name you expect. If a recipe name is longer than its placeholder, the name is truncated to fit.
- The tool automatically tries to swap recipes between slots if one recipe name fits better in another slot's placeholder space.
- Use distinct, recognizable placeholder text that would not appear in actual recipe data (e.g., "Recipe Title One Lorem Ipsum Dolor Sit Amet").
- For ingredients, more lines with more placeholder text per line gives the tool more room to display all ingredients.
Every option below can also be set in config.ini. Command-line values override config file values.
| Flag | Config key | Default | Description |
|---|---|---|---|
-c, --my-config |
-- | config.ini |
Path to configuration file. |
--url |
url |
(required) | Full URL of the Tandoor server. |
--token |
token |
(required) | Tandoor API token. |
--log |
log |
info |
Logging level (info or debug). |
--cache |
cache |
240 |
Minutes to cache API results; 0 to disable. |
--recipes |
recipes |
(none) | JSON object of recipe search parameters. |
--filters |
filter |
[] |
CustomFilter IDs to source recipes from. |
--plan_type |
plan_type |
[] |
MealType IDs to source recipes from meal plans. |
--choices |
choices |
5 |
Number of recipes to select. |
--keyword |
keyword |
[] |
Keyword-based rules. |
--food |
food |
[] |
Food-based rules. |
--book |
book |
[] |
Book-based rules. |
--rating |
rating |
[] |
Rating-based rules. |
--cookedon |
cookedon |
[] |
Last-cooked-date rules. |
--createdon |
createdon |
[] |
Creation-date rules. |
--include_children |
-- | true |
Include child keywords/foods in rule matching. |
--create_mp |
create_mp |
false |
Create meal plan entries for selected recipes. |
--mp_type |
mp_type |
(required with create_mp) |
MealType ID for created plans. |
--mp_date |
mp_date |
0days |
Date for meal plan entries. |
--mp_note |
mp_note |
Created by: Tandoor Menu Generator. |
Note text for meal plan entries. |
--share_with |
share_with |
[] |
User IDs to share the meal plan with. |
--cleanup_mp |
cleanup_mp |
false |
Delete uncooked meal plans before creating new ones. |
--cleanup_date |
cleanup_date |
-7days |
Start date for cleanup. |
--create_file |
create_file |
false |
Generate a menu file from an SVG template. |
--file_template |
file_template |
(required with create_file) |
SVG template filename (in templates/ directory). |
--file_format |
file_format |
PNG |
Output format: GIF, JPG, PNG, or PDF. |
--output_dir |
output_dir |
templates/ |
Output directory for the generated file. |
--fonts |
fonts |
[] |
Custom font definitions for the SVG template. |
--replace_text |
replace_text |
(none) | Template placeholder-to-data mapping. |
--separator |
separator |
' - ' |
Separator for concatenating ingredients. |
This means your rules are contradictory or too restrictive for the available recipes. Try:
- Lowering the
countin one or more rules. - Changing
>=to<=or removing a rule entirely. - Increasing
choicesto give the solver more flexibility. - Running with
--log debugto see how many recipes matched each rule.
The total number of available recipes (after filtering) is less than choices. Either:
- Add more recipes to Tandoor.
- Reduce the
choicescount. - Broaden your recipe search parameters or remove filters.
Your API token may be invalid or expired. Generate a new token in Tandoor under Settings > API Tokens.
Check that your url setting is correct. It should be the base URL of your Tandoor instance (e.g., https://tandoor.example.com or https://tandoor.example.com:8080). Do not include /api/ -- the tool appends that automatically.
The solver introduces randomness, but with tight constraints there may be few valid solutions. Try:
- Loosening your rules to allow more combinations.
- Disabling the cache (
cache: 0) if your recipe data has changed recently. - Adding more recipes to Tandoor to give the solver a larger pool.
The template system truncates recipe names and ingredients to fit within the length of the placeholder text. To fix this:
- Use longer placeholder strings in your SVG template.
- Add more ingredient lines to give the tool more space.
- Use shorter separator text (e.g.,
separator: ', 'instead of the default' - ').
Use cron (Linux/macOS) or Task Scheduler (Windows) to run the tool automatically:
# Run every Sunday at 6 PM
0 18 * * 0 cd /path/to/tandoor-menu-generator && python create_menu.pyNavigate to the item in your Tandoor web interface and look at the URL bar. The number at the end of the URL is the ID. For example:
https://tandoor.example.com/list/keyword/47/-- keyword ID is 47https://tandoor.example.com/list/food/15/-- food ID is 15
Contributions are welcome. Please open an issue or pull request on GitHub.
This project is open source. See the repository for license details.