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 := "