Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add media to the exported zip #488

Merged
merged 1 commit into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions internal/models/recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,14 @@ func (r *Recipe) Scale(yield int16) {

// Schema creates the schema representation of the Recipe.
func (r *Recipe) Schema() RecipeSchema {
var img string
var thumbnail string
images := make([]string, 0, len(r.Images))
if len(r.Images) > 0 {
img = r.Images[0].String() + app.ImageExt
thumbnail = app.Config.Address() + "/data/images/" + r.Images[0].String()

for _, img := range r.Images {
images = append(images, app.Config.Address()+"/data/images/"+img.String()+app.ImageExt)
}
}

instructions := make([]HowToItem, 0, len(r.Instructions))
Expand All @@ -283,14 +288,18 @@ func (r *Recipe) Schema() RecipeSchema {
video := &Videos{Values: make([]VideoObject, 0, len(r.Videos))}
for i, v := range r.Videos {
u := app.Config.Address() + "/data/videos/" + v.ID.String() + app.VideoExt
if v.ContentURL == "" {
v.ContentURL = u
}

video.Values = append(video.Values, VideoObject{
AtType: "VideoObject",
Name: "Video #" + strconv.Itoa(i+1),
Description: "A video showing how to cook " + r.Name,
ThumbnailURL: nil,
ContentURL: u,
EmbedURL: u,
ID: v.ID,
ThumbnailURL: v.ThumbnailURL,
ContentURL: v.ContentURL,
EmbedURL: v.EmbedURL,
UploadDate: v.UploadDate,
Duration: v.Duration,
Expires: time.Now().AddDate(1000, 0, 0),
Expand All @@ -309,13 +318,13 @@ func (r *Recipe) Schema() RecipeSchema {
DatePublished: r.CreatedAt.Format(time.DateOnly),
Description: &Description{Value: r.Description},
Keywords: &Keywords{Values: strings.Join(r.Keywords, ",")},
Image: &Image{Value: img},
Image: &Image{Value: strings.Join(images, ";")},
Ingredients: &Ingredients{Values: r.Ingredients},
Instructions: &Instructions{Values: instructions},
Name: r.Name,
NutritionSchema: r.Nutrition.Schema(strconv.Itoa(int(r.Yield))),
PrepTime: formatDuration(r.Times.Prep),
ThumbnailURL: &ThumbnailURL{Value: img},
ThumbnailURL: &ThumbnailURL{Value: thumbnail},
Tools: &Tools{Values: r.Tools},
TotalTime: formatDuration(r.Times.Total),
Yield: &Yield{Value: r.Yield},
Expand Down
4 changes: 2 additions & 2 deletions internal/models/recipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1045,8 +1045,8 @@ func TestRecipe_Schema(t *testing.T) {
if schema.Keywords != nil && schema.Keywords.Values != "kw1,kw2,kw3" {
t.Errorf("wanted keywords 'kw1,kw2,kw3' but got %q", schema.Keywords)
}
v = imageUUID.String()
if schema.Image != nil && schema.Image.Value != v+app.ImageExt {
v = app.Config.Address() + "/data/images/" + imageUUID.String() + app.ImageExt
if schema.Image != nil && schema.Image.Value != v {
t.Errorf("wanted uuid %q but got %q", v, schema.Image.Value)
}

Expand Down
13 changes: 8 additions & 5 deletions internal/models/schema-recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,7 @@ type Image struct {

// MarshalJSON encodes the image.
func (i *Image) MarshalJSON() ([]byte, error) {
s := i.Value
if s != "" {
s = app.Config.Address() + "/data/images/" + s
}
return json.Marshal(s)
return json.Marshal(i.Value)
}

// UnmarshalJSON decodes the image according to the schema (https://schema.org/image).
Expand All @@ -531,6 +527,11 @@ func (i *Image) UnmarshalJSON(data []byte) error {

switch x := v.(type) {
case string:
baseURL := app.Config.Address()
if strings.HasPrefix(x, baseURL) {
x = strings.Replace(x, baseURL+"/data/images/", "", 1)
x = strings.TrimSuffix(x, app.ImageExt)
}
i.Value = x
case []any:
if len(x) > 0 {
Expand Down Expand Up @@ -879,6 +880,8 @@ func (v *Videos) UnmarshalJSON(data []byte) error {
vid.IsIFrame = true
}

vid.ContentURL = strings.Replace(vid.ContentURL, app.Config.Address(), "", 1)

v.Values = append(v.Values, vid)
}
}
Expand Down
8 changes: 1 addition & 7 deletions internal/models/schema-recipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,13 +797,7 @@ func TestRecipeSchema_Marshal(t *testing.T) {
t.Errorf("got keywords %q; want %q", v, rs.Keywords.Values)
}
case "image":
want := "/data/images/" + rs.Image.Value
s := v.(string)
_, after, ok := strings.Cut(v.(string), "/")
if ok {
s = "/" + after
}
if s != want {
if v.(string) != rs.Image.Value {
t.Errorf("got image %q; want %q", v, rs.Image.Value)
}
case "recipeIngredient":
Expand Down
75 changes: 53 additions & 22 deletions internal/services/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ type Files struct {
}

type exportData struct {
recipeName string
recipeImage uuid.UUID
data []byte
recipeName string
data []byte
images []uuid.UUID
videos []models.VideoObject
}

// BackupGlobal backs up the whole database to the backup directory.
Expand Down Expand Up @@ -461,6 +462,7 @@ func (f *Files) ExportRecipes(recipes models.Recipes, fileType models.FileType,
progress <- i
}

// Add recipe to zip
out, err := writer.Create(e.recipeName + "/recipe" + fileType.Ext())
if err != nil {
return nil, err
Expand All @@ -470,6 +472,46 @@ func (f *Files) ExportRecipes(recipes models.Recipes, fileType models.FileType,
if err != nil {
return nil, err
}

// Add images to zip
for _, u := range e.images {
fileName := u.String() + app.ImageExt

file, err := os.ReadFile(filepath.Join(app.ImagesDir, fileName))
if err != nil {
return nil, err
}

out, err = writer.Create(e.recipeName + "/" + fileName)
if err != nil {
return nil, err
}

_, err = out.Write(file)
if err != nil {
return nil, err
}
}

// Add videos to zip
for _, video := range slices.DeleteFunc(e.videos, func(v models.VideoObject) bool { return v.ID == uuid.Nil }) {
fileName := video.ID.String() + app.VideoExt

file, err := os.ReadFile(filepath.Join(app.VideosDir, fileName))
if err != nil {
return nil, err
}

out, err = writer.Create(e.recipeName + "/" + fileName)
if err != nil {
return nil, err
}

_, err = out.Write(file)
if err != nil {
return nil, err
}
}
}
case models.PDF:
processed := make(map[string]struct{})
Expand Down Expand Up @@ -516,15 +558,11 @@ func exportRecipesJSON(recipes models.Recipes) []exportData {
continue
}

var img uuid.UUID
if len(r.Images) > 0 {
img = r.Images[0]
}

data[i] = exportData{
recipeName: r.Name,
recipeImage: img,
data: xb,
recipeName: r.Name,
data: xb,
images: r.Images,
videos: r.Videos,
}
}
return data
Expand All @@ -533,15 +571,9 @@ func exportRecipesJSON(recipes models.Recipes) []exportData {
func exportRecipesPDF(recipes models.Recipes) []exportData {
data := make([]exportData, len(recipes))
for i, r := range recipes {
var img uuid.UUID
if len(r.Images) > 0 {
img = r.Images[0]
}

data[i] = exportData{
recipeName: r.Name,
recipeImage: img,
data: recipeToPDF(&r),
recipeName: r.Name,
data: recipeToPDF(&r),
}
}
return data
Expand Down Expand Up @@ -998,9 +1030,8 @@ func (f *Files) ExportCookbook(cookbook models.Cookbook, fileType models.FileTyp

func exportCookbookToPDF(cookbook *models.Cookbook) exportData {
return exportData{
recipeName: cookbook.Title,
recipeImage: cookbook.Image,
data: cookbookToPDF(cookbook),
recipeName: cookbook.Title,
data: cookbookToPDF(cookbook),
}
}

Expand Down
68 changes: 68 additions & 0 deletions internal/services/files_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/google/uuid"
"github.com/reaper47/recipya/internal/app"
"github.com/reaper47/recipya/internal/integrations"
"github.com/reaper47/recipya/internal/models"
"github.com/reaper47/recipya/internal/utils/extensions"
"io"
"log/slog"
"mime/multipart"
"os"
"path/filepath"
"slices"
"strings"
Expand Down Expand Up @@ -201,97 +203,163 @@
recipes = make(models.Recipes, 0, len(zr.File))
)

for _, zf := range zr.File {
if strings.Contains(zf.Name, "__MACOSX") {
continue
}

if imageUUID != uuid.Nil && (zf.FileInfo().IsDir() || (recipeNumber > 0 && len(recipes[recipeNumber-1].Images) == 0)) {
recipes[recipeNumber-1].Images = append(recipes[recipeNumber-1].Images, imageUUID)
imageUUID = uuid.Nil
}

validImageFormats := []string{".jpg", ".jpeg", ".png"}
if imageUUID == uuid.Nil && slices.Contains(validImageFormats, filepath.Ext(zf.Name)) {
imageFile, err := zf.Open()
if err != nil {
slog.Error("Failed to open image file", "file", zf, "error", err)
continue
}

if zf.FileInfo().Size() < 1<<12 {
_ = imageFile.Close()
continue
}

imageUUID, err = f.UploadImage(imageFile)
if err != nil {
slog.Error("Failed to upload image", "file", zf, "error", err)
}

_ = imageFile.Close()
continue
}

openedFile, err := zf.Open()
if err != nil {
slog.Error("Failed to open file", "file", zf, "error", err)
continue
}

switch strings.ToLower(filepath.Ext(zf.Name)) {
case models.CML.Ext():
xr := models.NewRecipesFromCML(openedFile, nil, f.UploadImage)
if len(xr) > 0 {
recipes = append(recipes, xr...)
recipeNumber += len(xr)
}
case models.Crumb.Ext():
recipes = append(recipes, models.NewRecipeFromCrouton(openedFile, f.UploadImage))
recipeNumber++
case models.JSON.Ext():
xr, err := f.extractJSONRecipes(openedFile)
if err != nil {
_ = openedFile.Close()
slog.Error("Failed to extract", "file", zf, "error", err)
continue
}

recipes = append(recipes, xr...)
recipeNumber += len(xr)
case models.MXP.Ext():
xr := models.NewRecipesFromMasterCook(openedFile)
if len(xr) > 0 {
recipes = append(recipes, xr...)
recipeNumber += len(xr)
}
case models.Paprika.Ext():
xr := f.processPaprikaRecipes(openedFile, nil)
if len(xr) > 0 {
recipes = append(recipes, xr...)
recipeNumber += len(xr)
}
case models.TXT.Ext():
recipe, err := models.NewRecipeFromTextFile(openedFile)
if errors.Is(err, models.ErrIsAccuChef) {
_ = openedFile.Close()
xr := models.NewRecipesFromAccuChef(openedFile)
recipes = append(recipes, xr...)
recipeNumber += len(xr)
continue
} else if errors.Is(err, models.ErrIsEasyRecipeDeluxe) {
_ = openedFile.Close()
xr := models.NewRecipesFromEasyRecipeDeluxe(openedFile)
recipes = append(recipes, xr...)
recipeNumber += len(xr)
continue
} else if err != nil {
_ = openedFile.Close()
slog.Error("Could not create recipe from text file", "file", zf.Name, "error", err)
continue
}
recipes = append(recipes, recipe)
recipeNumber++
case app.ImageExt:
parts := strings.Split(zf.Name, "/")

imagePath := filepath.Join(app.ImagesDir, parts[len(parts)-1])
_, err = os.Stat(imagePath)
Dismissed Show dismissed Hide dismissed
if errors.Is(err, os.ErrNotExist) {
dest, err := os.Create(imagePath)
Dismissed Show dismissed Hide dismissed
if err != nil {
slog.Error("Failed to create image file", "file", zf, "error", err)
continue
}

_, err = io.Copy(dest, openedFile)
if err != nil {
_ = dest.Close()
slog.Error("Failed to copy image file", "file", zf, "error", err)
continue
}

_ = dest.Close()
}

thumbnailPath := filepath.Join(app.ThumbnailsDir, parts[len(parts)-1])
_, err = os.Stat(thumbnailPath)
Dismissed Show dismissed Hide dismissed
if errors.Is(err, os.ErrNotExist) {
thumbnail, err := os.Create(thumbnailPath)
Dismissed Show dismissed Hide dismissed
if err != nil {
slog.Error("Failed to create thumbnail file", "file", zf, "error", err)
continue
}

imageFile, err := os.Open(imagePath)
Dismissed Show dismissed Hide dismissed
if err != nil {
slog.Error("Failed to create image file", "file", zf, "error", err)
continue
}

_, err = io.Copy(thumbnail, imageFile)
if err != nil {
_ = imageFile.Close()
_ = thumbnail.Close()
slog.Error("Failed to copy thumbnail file", "file", zf, "error", err)
continue
}

_ = imageFile.Close()
_ = thumbnail.Close()
}

case app.VideoExt:
parts := strings.Split(zf.Name, "/")
path := filepath.Join(app.VideosDir, parts[len(parts)-1])
_, err = os.Stat(path)
Dismissed Show dismissed Hide dismissed
if errors.Is(err, os.ErrNotExist) {
dest, err := os.Create(path)
Dismissed Show dismissed Hide dismissed
if err != nil {
slog.Error("Failed to create video file", "file", zf, "error", err)
continue
}

_, err = io.Copy(dest, openedFile)
if err != nil {
slog.Error("Failed to copy video file", "file", zf, "error", err)
continue
}
}
}

_ = openedFile.Close()
Expand Down
Loading