Import Valdemarsro favorites into Mealie via API
This commit is contained in:
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Import Valdemarsro favorites into Mealie (deduplicated by source URL).
|
||||||
|
|
||||||
|
This script imports a fixed list of favorite recipes from Valdemarsro by URL,
|
||||||
|
skipping recipes already present in Mealie.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_URL = "http://10.0.0.142:9925"
|
||||||
|
SECRETS = Path("/Volumes/homeassistant/secrets.yaml")
|
||||||
|
|
||||||
|
FAVORITE_SLUGS = [
|
||||||
|
"hjemmelavet-pizza",
|
||||||
|
"lasagne",
|
||||||
|
"musli-opskrift",
|
||||||
|
"lakselasagne",
|
||||||
|
"mexicansk-burger-med-hjemmelavet-guacamole",
|
||||||
|
"grontsagsfad",
|
||||||
|
"humus",
|
||||||
|
"indisk_curry_med_kylling",
|
||||||
|
"pariserbof",
|
||||||
|
"skipperlabskovs",
|
||||||
|
"pizzasnegle",
|
||||||
|
"luksus-stjerneskud",
|
||||||
|
"jordbaer-og-fetasalat-med-glaserede-pecannoedder",
|
||||||
|
"spaghetti-bolognese",
|
||||||
|
"kylling-med-cornflakes",
|
||||||
|
"tarteletter-hoens-asparges",
|
||||||
|
"one-pot-pasta",
|
||||||
|
"cacio-e-pepe",
|
||||||
|
"citronpasta",
|
||||||
|
"blomkaalssalat",
|
||||||
|
"koedsovs-onepotpasta",
|
||||||
|
"kikaertegryde",
|
||||||
|
"moerbradboeffer-med-bloede-loeg",
|
||||||
|
"stegt-spidskaal",
|
||||||
|
"bagt-kylling",
|
||||||
|
"ristede-kartoffelskiver-fad",
|
||||||
|
"vikingegryde",
|
||||||
|
"spidskaalssalat-opskrift",
|
||||||
|
"kylling-med-parmesan",
|
||||||
|
"bagt-broccoli",
|
||||||
|
"pastasalat-med-pesto",
|
||||||
|
"nachos-bowl",
|
||||||
|
"barbecuesauce",
|
||||||
|
"congee-rissuppe-kylling",
|
||||||
|
"macaroni-and-cheese",
|
||||||
|
"halloween-dessert",
|
||||||
|
"feta-pasta-med-tomat",
|
||||||
|
"tortellini-i-fad",
|
||||||
|
"flyvende-jacob",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def read_token() -> str:
|
||||||
|
for line in SECRETS.read_text().splitlines():
|
||||||
|
if line.strip().startswith("mealie_bearer_token:"):
|
||||||
|
return line.split(":", 1)[1].strip().strip('"')
|
||||||
|
raise RuntimeError("mealie_bearer_token not found in secrets.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
def api_request(path: str, token: str, method: str = "GET", payload: dict | None = None) -> dict:
|
||||||
|
headers = {"Authorization": token}
|
||||||
|
data = None
|
||||||
|
if payload is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
data = json.dumps(payload).encode("utf-8")
|
||||||
|
|
||||||
|
req = urllib.request.Request(f"{BASE_URL}{path}", headers=headers, data=data, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as resp:
|
||||||
|
raw = resp.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_existing_org_urls(token: str) -> set[str]:
|
||||||
|
urls: set[str] = set()
|
||||||
|
page = 1
|
||||||
|
per_page = 100
|
||||||
|
|
||||||
|
while True:
|
||||||
|
path = f"/api/recipes?page={page}&perPage={per_page}"
|
||||||
|
payload = api_request(path, token)
|
||||||
|
items = payload.get("items", []) or []
|
||||||
|
if not items:
|
||||||
|
break
|
||||||
|
|
||||||
|
for recipe in items:
|
||||||
|
org_url = (recipe.get("orgURL") or "").strip()
|
||||||
|
if org_url:
|
||||||
|
urls.add(org_url.rstrip("/"))
|
||||||
|
|
||||||
|
if len(items) < per_page:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def import_recipe_by_url(token: str, url: str) -> tuple[bool, str]:
|
||||||
|
try:
|
||||||
|
api_request(
|
||||||
|
"/api/recipes/create/url",
|
||||||
|
token,
|
||||||
|
method="POST",
|
||||||
|
payload={"url": url, "includeTags": True},
|
||||||
|
)
|
||||||
|
return True, "imported"
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = ""
|
||||||
|
try:
|
||||||
|
body = exc.read().decode("utf-8", errors="ignore")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if exc.code == 422:
|
||||||
|
return False, f"422 {body[:200]}"
|
||||||
|
return False, f"HTTP {exc.code} {body[:200]}"
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return False, str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
token = read_token()
|
||||||
|
urls = [f"https://www.valdemarsro.dk/{slug}/" for slug in FAVORITE_SLUGS]
|
||||||
|
|
||||||
|
existing_urls = fetch_existing_org_urls(token)
|
||||||
|
|
||||||
|
to_import = [u for u in urls if u.rstrip("/") not in existing_urls]
|
||||||
|
skipped = [u for u in urls if u.rstrip("/") in existing_urls]
|
||||||
|
|
||||||
|
imported: list[str] = []
|
||||||
|
failed: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
for url in to_import:
|
||||||
|
ok, msg = import_recipe_by_url(token, url)
|
||||||
|
if ok:
|
||||||
|
imported.append(url)
|
||||||
|
else:
|
||||||
|
failed.append((url, msg))
|
||||||
|
|
||||||
|
print(f"TOTAL_LISTED={len(urls)}")
|
||||||
|
print(f"SKIPPED_EXISTING={len(skipped)}")
|
||||||
|
print(f"IMPORTED={len(imported)}")
|
||||||
|
print(f"FAILED={len(failed)}")
|
||||||
|
|
||||||
|
if skipped:
|
||||||
|
print("-- skipped existing --")
|
||||||
|
for url in skipped:
|
||||||
|
print(url)
|
||||||
|
|
||||||
|
if imported:
|
||||||
|
print("-- imported --")
|
||||||
|
for url in imported:
|
||||||
|
print(url)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
print("-- failed --")
|
||||||
|
for url, reason in failed:
|
||||||
|
print(url)
|
||||||
|
print(f" reason: {reason}")
|
||||||
|
|
||||||
|
# Exit non-zero only if everything failed.
|
||||||
|
if failed and not imported:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"count": 7, "items": [{"date": "2026-04-28", "recipe": {"name": "Lasagne", "slug": "lasagne"}}, {"date": "2026-04-27", "recipe": {"name": "Kylling i cremet sennepssauce", "slug": "kylling-i-cremet-sennepssauce"}}, {"date": "2026-04-26", "recipe": {"name": "Lasagne", "slug": "lasagne"}}, {"date": "2026-04-25", "recipe": {"name": "Spr\u00f8de for\u00e5rsruller", "slug": "sprode-forarsruller"}}, {"date": "2026-04-24", "recipe": {"name": "Marry Me Chicken", "slug": "marry-me-chicken"}}, {"date": "2026-04-22", "recipe": {"name": "Kylling i cremet sennepssauce", "slug": "kylling-i-cremet-sennepssauce"}}, {"date": "2026-04-23", "recipe": {"name": "K\u00e5lfad med hakket oksek\u00f8d", "slug": "kalfad-med-hakket-oksekod"}}]}
|
{"count": 6, "items": [{"date": "2026-04-28", "recipe": {"name": "Lasagne", "slug": "lasagne"}}, {"date": "2026-04-27", "recipe": {"name": "Kylling i cremet sennepssauce", "slug": "kylling-i-cremet-sennepssauce"}}, {"date": "2026-04-26", "recipe": {"name": "Lasagne", "slug": "lasagne"}}, {"date": "2026-04-24", "recipe": {"name": "Marry Me Chicken", "slug": "marry-me-chicken"}}, {"date": "2026-04-22", "recipe": {"name": "Kylling i cremet sennepssauce", "slug": "kylling-i-cremet-sennepssauce"}}, {"date": "2026-04-23", "recipe": {"name": "K\u00e5lfad med hakket oksek\u00f8d", "slug": "kalfad-med-hakket-oksekod"}}]}
|
||||||
Reference in New Issue
Block a user