feat: Tandoor integration — settings, test connection, import, duplicate detection

Add TandoorClient (app/tandoor.py) with full recipe creation, image upload,
and duplicate detection via the Tandoor REST API. Settings page now has
separate Mealie and Tandoor sections. Import page shows both send buttons
based on which services are configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:29:58 +01:00
parent f7810ba33d
commit 458b1e362a
6 changed files with 476 additions and 68 deletions
+48 -29
View File
@@ -1,37 +1,38 @@
# Recipe Importer
Docker container for importing recipes from Hungarian websites into [Mealie](https://mealie.io/) (Tandoor support planned).
Docker container for importing recipes from Hungarian websites into [Mealie](https://mealie.io/) and [Tandoor Recipes](https://tandoor.dev/).
**Problem**: Mealie's built-in URL import cannot parse ingredients and instructions from Hungarian recipe sites like mindmegette.hu — it imports the title and image but shows "Could not detect ingredients / instructions".
**Problem**: Mealie's and Tandoor's built-in URL import cannot parse ingredients and instructions from Hungarian recipe sites like mindmegette.hu.
**Solution**: This container provides a web UI that scrapes Hungarian recipe pages with site-specific parsers, lets you review and edit the extracted data, then pushes it to Mealie via its REST API.
**Solution**: This container provides a web UI that scrapes Hungarian recipe pages with site-specific parsers, lets you review and edit the extracted data, then pushes it to Mealie and/or Tandoor via their REST APIs.
## Architecture
```
┌─────────────────────────────────────────────────┐
│ recipe-importer container (:8000) │
│ │
│ Flask + Gunicorn │
│ ├── /settings → Configure Mealie connection
│ ├── /import → Paste URL, scrape, review │
│ ├── /scrape → AJAX: parse recipe HTML │
│ ├── /send → AJAX: push to Mealie API │
── /health → Health check
Modules:
├── app/config.py → JSON config persistence
│ ├── app/scraper.py → Site-specific parsers │
── app/mealie.py Mealie REST API client
└───────────────────┬─────────────────────────────┘
HTTP
┌──────────────────┐
│ Mealie instance │
│ POST /api/recipes│
PATCH /api/...
│ PUT /api/.../img
└──────────────────┘
┌──────────────────────────────────────────────────────
│ recipe-importer container (:8000)
│ Flask + Gunicorn
│ ├── /settings → Configure Mealie & Tandoor
│ ├── /import → Paste URL, scrape, review
│ ├── /scrape → AJAX: parse recipe HTML
│ ├── /send → AJAX: push to Mealie API
── /send-tandoor → AJAX: push to Tandoor API
└── /health → Health check
Modules:
│ ├── app/config.py JSON config persistence
── app/scraper.py → Site-specific parsers
│ ├── app/mealie.py → Mealie REST API client │
│ └── app/tandoor.py → Tandoor REST API client
└───────────────────┬──────────────┬───────────────────┘
│ HTTP │ HTTP
▼ ▼
┌──────────────┐ ┌───────────────┐
Mealie │ │ Tandoor
│ POST /api/.. │ │ POST /api/..
│ PUT /api/.. │ │ PUT /api/.. │
└──────────────┘ └───────────────┘
```
## Supported Sites
@@ -76,6 +77,19 @@ The importer uses the Mealie REST API:
Authentication uses a long-lived API token (Bearer header), created in Mealie at *Profile → API Tokens*.
## Tandoor API Integration
The importer uses the Tandoor REST API:
1. **POST** `/api/recipe/` — create the full recipe in one call (name, description, source_url, steps with nested ingredients)
2. **PUT** `/api/recipe/{id}/image/` — upload the recipe image
**Step-based ingredients**: Tandoor nests ingredients inside steps. All ingredients are attached to the first step. Units and foods are auto-created by name (no separate resolution needed). Ingredient groups use `is_header: true` on a header entry.
**Duplicate detection**: Before import, searches Tandoor by title and checks the `source_url` field to detect already-imported recipes.
Authentication uses an API token (Bearer header), created in Tandoor at *Settings → API Browser → Auth Token*.
## Configuration
All settings are persisted to `/data/config.json` (mounted as a Docker volume).
@@ -84,6 +98,8 @@ All settings are persisted to `/data/config.json` (mounted as a Docker volume).
|---------|-------------|
| `mealie_url` | Full URL to Mealie instance (e.g. `https://mealie.example.com`) |
| `mealie_api_key` | Mealie API token |
| `tandoor_url` | Full URL to Tandoor instance (e.g. `https://recipes.example.com`) |
| `tandoor_api_key` | Tandoor API token |
## Deployment
@@ -92,7 +108,7 @@ All settings are persisted to `/data/config.json` (mounted as a Docker volume).
```yaml
services:
recipe-importer:
image: gitea.dooplex.hu/admin/recipe-importer:0.1.7
image: gitea.dooplex.hu/admin/recipe-importer:0.1.9
container_name: recipe-importer
restart: unless-stopped
ports:
@@ -101,6 +117,8 @@ services:
- recipe-data:/data
environment:
- SECRET_KEY=change-me-in-production
- MEALIE_INTERNAL_URL=http://mealie:9000
- TANDOOR_INTERNAL_URL=http://tandoor:8080
volumes:
recipe-data:
@@ -114,6 +132,7 @@ volumes:
| `DATA_DIR` | `/data` | Persistent storage path |
| `VERSION` | `dev` | Shown in the UI navbar |
| `MEALIE_INTERNAL_URL` | *(empty)* | Docker-internal Mealie URL (e.g. `http://mealie:9000`) to avoid Cloudflare hairpin |
| `TANDOOR_INTERNAL_URL` | *(empty)* | Docker-internal Tandoor URL (e.g. `http://tandoor:8080`) to avoid Cloudflare hairpin |
## Building
@@ -128,10 +147,10 @@ cd ~/build/recipe-importer
The UI is in Hungarian and uses a dark theme. The workflow is:
1. **Settings** (`/settings`) — Enter Mealie URL and API key, test connection
1. **Settings** (`/settings`) — Configure Mealie and/or Tandoor connection (URL + API key), test each connection
2. **Import** (`/import`) — Paste a recipe URL, click "Beolvasás" (Scrape)
3. **Review** — Edit structured ingredients (4-column: quantity, unit, food, note), add/remove ingredient groups, edit instructions
4. **Send** — Click "Importálás Mealie-be" to push to Mealie
4. **Send** — Click "Importálás Mealie-be" and/or "Importálás Tandoor-ba" to push to your configured services
## Tech Stack