Files
multiarch-docker-images/recipe4reader/recipe4reader.go

370 lines
9.6 KiB
Go

package main
import (
"encoding/json"
"fmt"
"image"
"image/jpeg"
"io"
"net/http"
"os"
"os/signal"
"regexp"
"strings"
"time"
epub "github.com/go-shiori/go-epub"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/google/uuid"
"github.com/nfnt/resize"
)
type Parameters struct {
KitchenOwlURL string
KitchenOwlKey string
KitchenOwlHousehold string
RecipeAuthors string
RecipeChapters string
RecipeOutput string
}
type Household struct {
Name string `json:"name"`
Photo string `json:"photo"`
}
type Item struct {
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Slug string `json:"-"`
}
type Tag struct {
Name string `json:"name"`
}
type Recipe struct {
Name string `json:"name"`
ID int `json:"id"`
Tags []Tag `json:"tags"`
Description string `json:"description"`
Items []Item `json:"items"`
}
// Converts a string to a slug suitable for referencing
func Slugify(s string) string {
// Convert to lowercase
result := strings.ToLower(s)
// Replace spaces with hyphens
result = strings.ReplaceAll(result, " ", "_")
// Return the slug
return result
}
// Converts Markdown text to HTML
func MarkdownToHTML(md string) string {
// Create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse([]byte(md))
// Create HTML renderer with extensions
htmlFlags := html.UseXHTML | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return string(markdown.Render(doc, renderer))
}
// Retrieves the household information from the KitchenOwl server
func FetchHousehold(param Parameters) (*Household, error) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, param.KitchenOwlURL+"/api/household/"+param.KitchenOwlHousehold, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+param.KitchenOwlKey)
req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("status code %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var household Household
if err := json.Unmarshal(body, &household); err != nil {
return nil, err
}
return &household, nil
}
// Retrieves an image from the KitchenOwl server, resize it and saves it to a temporary file
func FetchImage(param Parameters, imageName string) (*string, error) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, param.KitchenOwlURL+"/api/upload/"+imageName, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+param.KitchenOwlKey)
req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("status code %d", res.StatusCode)
}
// Open a file for writing
filename := "/tmp/" + imageName
file, err := os.Create(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Decode the image
img, _, err := image.Decode(res.Body)
if err != nil {
return nil, err
}
// Resize the image, encode it to JPEG and write to file
err = jpeg.Encode(file, resize.Resize(1000, 0, img, resize.Lanczos3), &jpeg.Options{Quality: 80})
if err != nil {
return nil, err
}
return &filename, nil
}
// Retrieves the recipes from the KitchenOwl server
func FetchRecipes(param Parameters) ([]Recipe, error) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, param.KitchenOwlURL+"/api/household/"+param.KitchenOwlHousehold+"/recipe", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+param.KitchenOwlKey)
req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("status code %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var recipes []Recipe
if err := json.Unmarshal(body, &recipes); err != nil {
return nil, err
}
for recipe := range recipes {
for item := range recipes[recipe].Items {
recipes[recipe].Items[item].Slug = Slugify(recipes[recipe].Items[item].Name)
}
}
return recipes, nil
}
// Create a new EBook with chapters and recipes
func CreateEbook(param Parameters, household Household, recipes []Recipe) (*epub.Epub, error) {
book, err := epub.NewEpub("KitchenOwl Recipes")
if err != nil {
fmt.Println("Error creating epub: ", err)
}
// Set author
if param.RecipeAuthors != "" {
book.SetAuthor(param.RecipeAuthors)
}
// Set description
book.SetDescription("Recipes from KitchenOwl server " + param.KitchenOwlURL)
book.SetLang("fr")
book.SetIdentifier("urn:uuid:" + uuid.NewMD5(uuid.NameSpaceDNS, []byte(param.KitchenOwlURL+"/api/household/"+param.KitchenOwlHousehold)).String())
// Add cover image
filename, err := FetchImage(param, household.Photo)
if err != nil {
fmt.Println("Error fetching cover image: ", err)
} else {
coverImagePath, err := book.AddImage(*filename, "cover.png")
if err != nil {
fmt.Println("Error setting cover: ", err)
} else {
err = book.SetCover(coverImagePath, "")
if err != nil {
fmt.Println("Error setting cover: ", err)
}
}
}
// Add an empty section at the beginning
content := "<h1>Info</h1><ul>"
content += "<li>Authors: " + param.RecipeAuthors + "</li>"
content += "<li>KitchenOwl URL: " + param.KitchenOwlURL + "</li>"
content += "<li>Household: " + household.Name + "</li>"
content += "<li>Total Recipes: " + fmt.Sprintf("%d", len(recipes)) + "</li>"
content += "<li>Created on: " + time.Now().Format(time.ANSIC) + "</li>"
content += "</ul>"
_, err = book.AddSection(content, "Info", "", "")
if err != nil {
fmt.Println("Error creating section: ", err)
}
// Process each chapter
for _, chapter := range strings.Split(param.RecipeChapters, ",") {
// Add a section for each specified chapter
section, err := book.AddSection("<h1>"+chapter+"</h1>", chapter, "", "")
if err != nil {
fmt.Println("Error creating section: ", err)
}
// Add recipes that belong to the current chapter
for _, recipe := range recipes {
hasTag := false
for _, tag := range recipe.Tags {
if strings.EqualFold(tag.Name, chapter) {
hasTag = true
break
}
}
if hasTag {
content := "<h1>" + recipe.Name + "</h1>"
content += "<h2>Ingrédients</h2>"
content += "<ul>"
for _, item := range recipe.Items {
content += "<li>" + item.Name
if item.Description != "" {
content += ": " + item.Description
}
content += "</li>"
}
content += "</ul>"
content += "<h2>Instructions</h2>"
// Replace @item_slug with item name and description
description := recipe.Description
if strings.Contains(description, "@") {
re := regexp.MustCompile(`@([^ ,]+)`)
description = re.ReplaceAllStringFunc(description, func(match string) string {
slug := strings.TrimPrefix(match, "@")
for _, item := range recipe.Items {
if item.Slug == slug {
return item.Name + " (" + item.Description + ")"
}
}
return match // return the original if not found
})
}
content += MarkdownToHTML(description)
_, err := book.AddSubSection(section, content, recipe.Name, "", "")
if err != nil {
fmt.Println("Error adding recipe section: ", err)
}
}
}
}
return book, nil
}
func main() {
// Capture interrupt signal for graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
fmt.Println("Got interrupt signal. Exiting...")
os.Exit(0)
}()
// Load parameters from the environment variables
param := &Parameters{
KitchenOwlURL: os.Getenv("KITCHENOWL-URL"),
KitchenOwlHousehold: os.Getenv("KITCHENOWL-HOUSEHOLD"),
KitchenOwlKey: os.Getenv("KITCHENOWL-KEY"),
RecipeAuthors: os.Getenv("RECIPE-AUTHORS"),
RecipeChapters: os.Getenv("RECIPE-CHAPTERS"),
RecipeOutput: os.Getenv("RECIPE-OUTPUT"),
}
// Fetch recipes
fmt.Printf("Fetching recipes...")
household, err := FetchHousehold(*param)
if err != nil {
fmt.Println("Error fetching household: ", err)
return
}
recipes, err := FetchRecipes(*param)
if err != nil {
fmt.Println("Error fetching recipes: ", err)
return
}
fmt.Printf(" %d recipes fetched\n", len(recipes))
// Output the recipes to screen if specified
if param.RecipeOutput == "" || param.RecipeOutput == "screen" {
// Print fetched recipes
fmt.Printf("Household: %s, Photo: %s\n", household.Name, household.Photo)
for _, recipe := range recipes {
fmt.Printf("Recipe: %s, ID: %d, Items: %s, Tags: %s, Description: %s\n", recipe.Name, recipe.ID, recipe.Items, recipe.Tags, recipe.Description)
}
return
}
// Create the EBook
fmt.Printf("Creating ebook...")
book, err := CreateEbook(*param, *household, recipes)
if err != nil {
fmt.Println("Error creating ebook: ", err)
return
}
fmt.Printf(" done\n")
// Write the EBook to file if specified
if strings.HasPrefix(param.RecipeOutput, "file:") {
filename := strings.TrimPrefix(param.RecipeOutput, "file:")
err := book.Write(filename)
if err != nil {
fmt.Println("Error writing epub to file: ", err)
} else {
fmt.Printf("Ebook written to %s\n", filename)
}
}
}