initial commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: mealie-setup
|
||||
description: Deploy and configure Mealie (self-hosted meal planner/recipe manager) — Docker deployment, nginx reverse proxy, batch recipe import, mobile app setup, and common pitfalls.
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [mealie, meal-planner, recipes, docker, self-hosting, nginx]
|
||||
---
|
||||
|
||||
# Mealie Setup
|
||||
|
||||
Mealie is a self-hosted recipe manager and meal planner. Web-based with PWA support for mobile, plus third-party Android/iOS apps (Ghee, Mealient, MealieSwift).
|
||||
|
||||
## Quick Deploy
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mealie \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:9925:9000 \
|
||||
-v /path/to/data:/app/data \
|
||||
ghcr.io/mealie-recipes/mealie:latest
|
||||
```
|
||||
|
||||
**⚠️ PORT PITFALL:** Mealie listens on **port 9000** internally, not 9925. The `-p` mapping must be `host:9000`, not `host:9925`. If you map `9925:9925`, the container starts but the proxy returns empty responses.
|
||||
|
||||
## BASE_URL (critical for invite links)
|
||||
|
||||
Without `BASE_URL`, Mealie generates invite links, password reset URLs, and notification links using `http://localhost:8080` — they'll always fail. Set it to the external URL including the port:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mealie \
|
||||
...
|
||||
-e BASE_URL=https://your.domain:3449 \
|
||||
ghcr.io/mealie-recipes/mealie:latest
|
||||
```
|
||||
|
||||
If invite links show "refused to connect", you forgot `BASE_URL`. Re-create the container with it set — data survives in the volume.
|
||||
|
||||
## Nginx Reverse Proxy
|
||||
|
||||
Mealie needs to be at the root path — subpath proxying (`/mealie/`) is not supported (JS framework limitation).
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen PORT ssl;
|
||||
server_name your.domain;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:9925;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## First Visit & Setup
|
||||
|
||||
- First visitor creates the admin account (no default credentials).
|
||||
- Seed **Foods** and **Units** databases: User menu → Manage Data → ensure orange button shows "Foods" → click Seed → do the same for Units. This enables smart ingredient parsing and shopping list generation.
|
||||
|
||||
## Mobile Access
|
||||
|
||||
- **PWA** (recommended, free): Open Mealie in Chrome Android → ⋮ → "Add to Home Screen". Full-screen with offline caching. All features (meal planner, shopping lists, cook mode) are free — unlike third-party apps.
|
||||
- **Ghee** (Play Store): **⚠️ Meal planner is a paid feature.** Recipe browsing and shopping lists work for free, but the meal calendar requires an in-app purchase. If "server unreachable" with SSL URL, try `http://192.168.50.X:9925` first — hairpin NAT or Android cleartext policies often block HTTPS on non-standard ports from apps.
|
||||
- **Mealient** (open source): GitHub release APK. Free, no paywalls.
|
||||
|
||||
## Batch Importing Recipes
|
||||
|
||||
Recipes are imported by URL via the scraper API. See `references/batch-import.md` for a curl/Python script.
|
||||
|
||||
**Scraper compatibility:** see `references/scraper-sites.md` for which recipe sites work reliably and workarounds for blocked sites.
|
||||
|
||||
**Recipe management (list, filter, delete):** see `references/recipe-management.md` for bulk CRUD via API — useful when you need to remove recipes by keyword (e.g., all pork/bacon recipes) or inspect what's in the library.
|
||||
|
||||
## Managing Recipes
|
||||
|
||||
Search, filter, and delete recipes in bulk via the API — useful for dietary cleanup, duplicate removal, and post-import curation. See `references/recipe-management.md` for the full API workflow (list, keyword-search, inspect ingredients, delete).
|
||||
|
||||
## Data Location
|
||||
|
||||
- Container data: `/app/data` (bind-mount to persistent storage)
|
||||
- Database: SQLite inside the data directory
|
||||
- Backups: copy the data directory
|
||||
@@ -0,0 +1,67 @@
|
||||
# Batch Recipe Import
|
||||
|
||||
Import recipes in bulk via the Mealie API. The scraper extracts full recipe data (ingredients, steps, images, times) from any supported URL.
|
||||
|
||||
## Python Script
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
MEALIE = "http://127.0.0.1:9925"
|
||||
EMAIL = "your@email.com"
|
||||
PASSWORD = "your-password"
|
||||
|
||||
# Authenticate
|
||||
resp = requests.post(
|
||||
f"{MEALIE}/api/auth/token",
|
||||
data={"username": EMAIL, "password": PASSWORD, "grant_type": ""},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
token = resp.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
# Recipe URLs (one per line)
|
||||
urls = [
|
||||
"https://www.bbcgoodfood.com/recipes/classic-lasagne",
|
||||
"https://www.budgetbytes.com/one-pot-creamy-cajun-chicken-pasta/",
|
||||
"https://www.recipetineats.com/thai-green-curry/",
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
resp = requests.post(
|
||||
f"{MEALIE}/api/recipes/create/url",
|
||||
json={"url": url},
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
if resp.status_code in (200, 201):
|
||||
print(f" OK: {url.split('/')[-2]}")
|
||||
else:
|
||||
print(f" FAIL: {url.split('/')[-2]} — {resp.status_code}")
|
||||
time.sleep(1.5) # Be polite to recipe sites
|
||||
```
|
||||
|
||||
## Bulk curl (no Python needed)
|
||||
|
||||
```bash
|
||||
# Get token
|
||||
TOKEN=$(curl -s -X POST "http://127.0.0.1:9925/api/auth/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=&username=you@email.com&password=yourpass" \
|
||||
| grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
# Import a single recipe
|
||||
curl -s -X POST "http://127.0.0.1:9925/api/recipes/create/url" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://www.bbcgoodfood.com/recipes/classic-lasagne"}'
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- URLs must be full recipe pages, not search results or category pages.
|
||||
- If a site returns HTTP 400 consistently, it likely blocks automated scrapers — see `scraper-sites.md` for alternatives.
|
||||
- First import may be slow (Mealie's scraper downloads the page, parses schema.org JSON-LD, and extracts structured data).
|
||||
- **Real-world success rate:** ~50-60% across mixed URLs. Expect roughly half to fail on the first pass. Rerun failures from a reliable source (BBC Good Food, RecipeTin Eats) to fill gaps.
|
||||
- **From Hermes:** use `execute_code` with `import requests` — the sandboxed Python environment can call `http://127.0.0.1:9925` directly. No need for curl or external scripts.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Recipe Management via API
|
||||
|
||||
Beyond batch importing, the Mealie API supports full CRUD for recipes — useful for bulk deletion, tag assignment, or fixing metadata.
|
||||
|
||||
## Authentication (same as batch import)
|
||||
|
||||
```python
|
||||
import requests
|
||||
M = "http://127.0.0.1:9925"
|
||||
token = requests.post(f"{M}/api/auth/token",
|
||||
data={"username": "email", "password": "pw", "grant_type": ""},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
).json()["access_token"]
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
```
|
||||
|
||||
## List all recipes
|
||||
|
||||
```python
|
||||
r = requests.get(f"{M}/api/recipes?page=1&perPage=100", headers=h)
|
||||
data = r.json() # keys: page, per_page, total, total_pages, items
|
||||
for recipe in data["items"]:
|
||||
print(f"[{recipe['id']}] {recipe['name']}")
|
||||
```
|
||||
|
||||
Pagination: increment `page` until `page > total_pages`.
|
||||
|
||||
## Delete recipes (single or bulk)
|
||||
|
||||
```python
|
||||
# Single
|
||||
r = requests.delete(f"{M}/api/recipes/{recipe_id}", headers=h)
|
||||
|
||||
# Bulk with keyword filter
|
||||
for recipe in data["items"]:
|
||||
if "pork" in recipe["name"].lower():
|
||||
r = requests.delete(f"{M}/api/recipes/{recipe['id']}", headers=h)
|
||||
print(f"Deleted: {recipe['name']} -> {r.status_code}")
|
||||
```
|
||||
|
||||
HTTP 200 = success. No undo — confirm your filter before running.
|
||||
|
||||
## Find by keyword
|
||||
|
||||
Check `name`, `slug`, `description`, and `recipeIngredient` for matching terms:
|
||||
|
||||
```python
|
||||
import json
|
||||
txt = json.dumps(recipe).lower()
|
||||
if "bacon" in txt:
|
||||
# recipe mentions bacon somewhere
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Recipe IDs are UUIDs (e.g. `52fe7e87-49c2-4cd4-b4f3-82ae7e1b102b`), not sequential integers.
|
||||
- The list endpoint returns limited fields; fetch individual recipes (`GET /api/recipes/{id}`) for full ingredient/instruction data.
|
||||
- Deletion is instant — no trash/recycle bin in Mealie.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Recipe Scraper Site Compatibility
|
||||
|
||||
Mealie's built-in recipe scraper extracts structured data from recipe websites. Compatibility varies — some sites block automated access (Cloudflare, aggressive bot detection), others work flawlessly.
|
||||
|
||||
## Reliable (high success rate)
|
||||
|
||||
| Site | Notes |
|
||||
|---|---|
|
||||
| **BBC Good Food** (bbcgoodfood.com) | Excellent. Fast parsing, full ingredient + step extraction. |
|
||||
| **RecipeTin Eats** (recipetineats.com) | Very reliable. Good image extraction. |
|
||||
| **Budget Bytes** (budgetbytes.com) | Works ~70% of time. Rate-limit sensitive — add 1-2s delay between imports. |
|
||||
| **Love and Lemons** (loveandlemons.com) | Works well. Vegan/vegetarian focus. |
|
||||
|
||||
## Unreliable / Blocked (low success rate)
|
||||
|
||||
| Site | Issue |
|
||||
|---|---|
|
||||
| **AllRecipes** (allrecipes.com) | Aggressive bot detection. HTTP 400 on most attempts. |
|
||||
| **Simply Recipes** (simplyrecipes.com) | Blocks automated scrapers. |
|
||||
| **Damn Delicious** (damndelicious.net) | Cloudflare or similar protection. Fails consistently. |
|
||||
| **Gimme Some Oven** (gimmesomeoven.com) | Same blocking issue as above. |
|
||||
|
||||
## Curated import strategy (near-100% success)
|
||||
|
||||
When batch-importing by cuisine or theme, search **only** BBC Good Food and RecipeTin Eats for individual recipe URLs. Restricting to these two sites yields near-100% scraper success, compared to ~50-60% when mixing sources.
|
||||
|
||||
**Workflow for cuisine batch imports:**
|
||||
1. Search `site:bbcgoodfood.com <cuisine> recipe` and `site:recipetineats.com <cuisine> recipe`
|
||||
2. Pick individual recipe URLs (not category/list pages — URLs must end in a recipe slug like `/easy-chicken-fajitas`)
|
||||
3. Run the batch import script from `references/batch-import.md` with 2-second delays
|
||||
4. Expect 90-100% success rate
|
||||
|
||||
**Real-world example:** A 20-recipe batch (10 Mexican, 10 Mediterranean) from BBC Good Food + RecipeTin Eats imported with 20/20 success (100%). The same approach with mixed sources typically yields 10-12 successes.
|
||||
|
||||
## Workarounds for blocked sites
|
||||
|
||||
1. **Manual entry:** Copy-paste ingredients and steps into Mealie's manual recipe form.
|
||||
2. **Screenshot + OCR:** Not implemented yet, but Mealie supports OCR recipe creation (requires OpenAI API key or local LLM).
|
||||
3. **Alternative source:** Many popular recipes exist on multiple sites — search BBC Good Food or RecipeTin Eats for a similar version.
|
||||
4. **Import from JSON:** If you have recipes in JSON-LD or schema.org format, use Mealie's JSON import endpoint.
|
||||
|
||||
## Testing a new site
|
||||
|
||||
```bash
|
||||
# Quick test — if this returns 400, the site is likely blocked
|
||||
curl -s -X POST "http://127.0.0.1:9925/api/recipes/create/url" \
|
||||
-H "Authorization: Bearer *** \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://example.com/recipe/test"}'
|
||||
```
|
||||
|
||||
A successful import returns 200/201 with recipe JSON. 400 usually means the scraper couldn't extract data from the page. 500 means the site blocked the request entirely.
|
||||
Reference in New Issue
Block a user