From e81a5ff3cc1b8f5b5255f1bb38ecc7ba79896c8a Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 11 Jan 2026 18:27:59 +0100 Subject: [PATCH] Added recipe4reader --- .github/workflows/build.yml | 2 +- recipe4reader/Dockerfile | 12 ++ recipe4reader/recipe4reader.go | 356 +++++++++++++++++++++++++++++++++ 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 recipe4reader/Dockerfile create mode 100644 recipe4reader/recipe4reader.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b241ce..7ed657d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - repository: [log-alert, immich-souvenirs, dnsupdater, webhook, restic-auto, shairport-sync] + repository: [recipe4reader, log-alert, immich-souvenirs, dnsupdater, webhook, restic-auto, shairport-sync] steps: - name: Checkout diff --git a/recipe4reader/Dockerfile b/recipe4reader/Dockerfile new file mode 100644 index 0000000..6e87fe7 --- /dev/null +++ b/recipe4reader/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.24-alpine AS builder + +WORKDIR $GOPATH/src/napnap75/recipe4reader/ +COPY recipe4reader.go . +RUN apk add --no-cache git gcc musl-dev \ + && go mod init github.com/napnap75/multiarch-docker-files/recipe4reader \ + && go get -d -v \ + && go build -ldflags="-w -s" -o /go/bin/recipe4reader + +FROM alpine:latest +COPY --from=builder /go/bin/recipe4reader /usr/bin/ +ENTRYPOINT ["/usr/bin/recipe4reader"] diff --git a/recipe4reader/recipe4reader.go b/recipe4reader/recipe4reader.go new file mode 100644 index 0000000..18d5368 --- /dev/null +++ b/recipe4reader/recipe4reader.go @@ -0,0 +1,356 @@ +package main + +import ( + "encoding/json" + "fmt" + "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" +) + +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"` +} + +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 +} + +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)) +} + +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 +} + +func FetchImage(param Parameters, image string) (*string, error) { + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(http.MethodGet, param.KitchenOwlURL+"/api/upload/"+image, 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/" + image + file, err := os.Create(filename) + if err != nil { + return nil, err + } + defer file.Close() + + // Dump the response body to the file + _, err = io.Copy(file, res.Body) + if err != nil { + return nil, err + } + + return &filename, nil +} + +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.Printf("Error creating epub: %v\n", err) + } + + // Set author + if param.RecipeAuthors != "" { + book.SetAuthor(param.RecipeAuthors) + } + + // Set description + book.SetDescription("Recipes from KitchenOwl server " + param.KitchenOwlURL) + + // Add cover image + filename, err := FetchImage(param, household.Photo) + if err != nil { + fmt.Printf("Error fetching cover image: %v\n", err) + } else { + coverImagePath, err := book.AddImage(*filename, "cover.png") + if err != nil { + fmt.Printf("Error setting cover: %v\n", err) + } else { + err = book.SetCover(coverImagePath, "") + if err != nil { + fmt.Printf("Error setting cover: %v\n", err) + } + } + } + + // Add an empty section at the beginning + content := "

Info

" + _, err = book.AddSection(content, "Info", "", "") + if err != nil { + fmt.Printf("Error creating section: %v\n", err) + } + + // Process each chapter + for _, chapter := range strings.Split(param.RecipeChapters, ",") { + // Add a section for each specified chapter + section, err := book.AddSection("

"+chapter+"

", chapter, "", "") + if err != nil { + fmt.Printf("Error creating section: %v\n", 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 := "

" + recipe.Name + "

" + + content += "

Ingrédients

" + content += "" + + content += "

Instructions

" + // 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) + + if strings.HasPrefix(recipe.Name, "Kimchi") { + fmt.Println(content) + } + + _, err := book.AddSubSection(section, content, recipe.Name, "", "") + if err != nil { + fmt.Printf("Error adding recipe section: %v\n", 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.Printf("Error fetching household: %v\n", err) + return + } + recipes, err := FetchRecipes(*param) + if err != nil { + fmt.Printf("Error fetching recipes: %v\n", 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.Printf("Error creating ebook: %v\n", err) + return + } + fmt.Println(" done") + + // 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.Printf("Error writing epub to file: %v\n", err) + } else { + fmt.Printf("Ebook written to %s\n", filename) + } + } +}