package v1 import ( "bytes" "encoding/base64" "encoding/json" "errors" "image/png" "io" "net/http" "net/url" "github.com/hay-kot/httpkit/errchain" "github.com/rs/zerolog/log" "github.com/sysadminsmedia/homebox/backend/internal/data/repo" "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "github.com/sysadminsmedia/homebox/backend/internal/web/adapters" "github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/writer/standard" _ "embed" ) //go:embed assets/QRIcon.png var qrcodeLogo []byte // HandleGenerateQRCode godoc // // @Summary Create QR Code // @Tags Items // @Produce json // @Param data query string false "data to be encoded into qrcode" // @Success 200 {string} string "image/jpeg" // @Router /v1/qrcode [GET] // @Security Bearer func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc { type query struct { // 4,296 characters is the maximum length of a QR code Data string `schema:"data" validate:"required,max=4296"` } return func(w http.ResponseWriter, r *http.Request) error { q, err := adapters.DecodeQuery[query](r) if err != nil { return err } image, err := png.Decode(bytes.NewReader(qrcodeLogo)) if err != nil { panic(err) } decodedStr, err := url.QueryUnescape(q.Data) if err != nil { return err } qrc, err := qrcode.New(decodedStr) if err != nil { return err } toWriteCloser := struct { io.Writer io.Closer }{ Writer: w, Closer: io.NopCloser(nil), } qrwriter := standard.NewWithWriter(toWriteCloser, standard.WithLogoImage(image)) // Return the QR code as a jpeg image w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Content-Disposition", "attachment; filename=qrcode.jpg") return qrc.Save(qrwriter) } } /* ImportRef string `json:"-"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"` Name string `json:"name" validate:"required,min=1,max=255"` Quantity int `json:"quantity"` Description string `json:"description" validate:"max=1000"` AssetID AssetID `json:"-"` // Edges LocationID uuid.UUID `json:"locationId"` LabelIDs []uuid.UUID `json:"labelIds"` */ type UPCITEMDBResponse struct { Code string `json:"code"` Total int `json:"total"` Offset int `json:"offset"` Items []struct { Ean string `json:"ean"` Title string `json:"title"` Description string `json:"description"` Upc string `json:"upc"` Brand string `json:"brand"` Model string `json:"model"` Color string `json:"color"` Size string `json:"size"` Dimension string `json:"dimension"` Weight string `json:"weight"` Category string `json:"category"` LowestRecordedPrice float64 `json:"lowest_recorded_price"` HighestRecordedPrice float64 `json:"highest_recorded_price"` Images []string `json:"images"` Offers []struct { Merchant string `json:"merchant"` Domain string `json:"domain"` Title string `json:"title"` Currency string `json:"currency"` ListPrice string `json:"list_price"` Price float64 `json:"price"` Shipping string `json:"shipping"` Condition string `json:"condition"` Availability string `json:"availability"` Link string `json:"link"` UpdatedT int `json:"updated_t"` } `json:"offers"` Asin string `json:"asin"` Elid string `json:"elid"` } `json:"items"` } type BARCODESPIDER_COMResponse struct { ItemResponse struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` } `json:"item_response"` ItemAttributes struct { Title string `json:"title"` Upc string `json:"upc"` Ean string `json:"ean"` ParentCategory string `json:"parent_category"` Category string `json:"category"` Brand string `json:"brand"` Model string `json:"model"` Mpn string `json:"mpn"` Manufacturer string `json:"manufacturer"` Publisher string `json:"publisher"` Asin string `json:"asin"` Color string `json:"color"` Size string `json:"size"` Weight string `json:"weight"` Image string `json:"image"` IsAdult string `json:"is_adult"` Description string `json:"description"` } `json:"item_attributes"` Stores []struct { StoreName string `json:"store_name"` Title string `json:"title"` Image string `json:"image"` Price string `json:"price"` Currency string `json:"currency"` Link string `json:"link"` Updated string `json:"updated"` } `json:"Stores"` } // HandleGenerateQRCode godoc // // @Summary Search EAN from Barcode // @Tags Items // @Produce json // @Param data query string false "barcode to be searched" // @Success 200 {object} []repo.BarcodeProduct // @Router /v1/getproductfromean [GET] // @Security Bearer func (ctrl *V1Controller) HandleProductSearchEAN(conf config.BarcodeAPIConf) errchain.HandlerFunc { type query struct { // 4,296 characters is the maximum length of a QR code EAN string `schema:"productEAN" validate:"required,max=4296"` } /*fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) { auth := services.NewContext(r.Context()) return ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID) } return adapters.CommandID("id", fn, http.StatusOK)*/ return func(w http.ResponseWriter, r *http.Request) error { q, err := adapters.DecodeQuery[query](r) if err != nil { return err } log.Info().Msg("========================" + q.EAN) // Search on UPCITEMDB var products []repo.BarcodeProduct // www.ean-search.org/: not free // Example code: dewalt 5035048748428 upcitemdb := func(iEan string) ([]repo.BarcodeProduct, error) { resp, err := http.Get("https://api.upcitemdb.com/prod/trial/lookup?upc=" + iEan) if err != nil { return nil, err } //We Read the response body on the line below. body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } //Convert the body to type string sb := string(body) log.Info().Msg("Response: " + sb) var result UPCITEMDBResponse if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer log.Error().Msg("Can not unmarshal JSON") } var res []repo.BarcodeProduct for _, it := range result.Items { var p repo.BarcodeProduct p.SearchEngineName = "upcitemdb.com" p.Barcode = iEan p.Item.Description = it.Description p.Item.Name = it.Title p.Manufacturer = it.Brand p.ModelNumber = it.Model if len(it.Images) != 0 { p.ImageURL = it.Images[0] } res = append(res, p) } return res, nil } ps, err := upcitemdb(q.EAN) if err != nil { log.Error().Msg("Can not retrieve product from upcitemdb.com" + err.Error()) } // Barcode spider implementation barcodespider := func(tokenAPI string, iEan string) ([]repo.BarcodeProduct, error) { if len(tokenAPI) == 0 { return nil, errors.New("no api token configured for barcodespider") } req, err := http.NewRequest( "GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil) if err != nil { return nil, err } req.Header.Add("token", tokenAPI) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } //We Read the response body on the line below. body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } //Convert the body to type string sb := string(body) log.Info().Msg("Response: " + sb) var result BARCODESPIDER_COMResponse if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer log.Error().Msg("Can not unmarshal JSON") } // TODO: check 200 code on HTTP repsonse. var p repo.BarcodeProduct p.Barcode = iEan p.SearchEngineName = "barcodespider.com" p.Item.Name = result.ItemAttributes.Title p.Item.Description = result.ItemAttributes.Description p.Manufacturer = result.ItemAttributes.Brand p.ModelNumber = result.ItemAttributes.Model p.ImageURL = result.ItemAttributes.Image var res []repo.BarcodeProduct res = append(res, p) return res, nil } ps2, err := barcodespider(conf.TokenBarcodespider, q.EAN) if err != nil { log.Error().Msg("Can not retrieve product from barcodespider.com: " + err.Error()) } // Merge everything. for i := range ps { products = append(products, ps[i]) } for i := range ps2 { products = append(products, ps2[i]) } // Retrieve images if possible for i := range products { p := &products[i] if len(p.ImageURL) == 0 { continue } res, err := http.Get(p.ImageURL) if err != nil { log.Warn().Msg("Cannot fetch image for URL: " + p.ImageURL + ": " + err.Error()) } defer res.Body.Close() // Read data of image bytes, err := io.ReadAll(res.Body) if err != nil { log.Warn().Msg(err.Error()) } // Convert to Base64 var base64Encoding string // Determine the content type of the image file mimeType := http.DetectContentType(bytes) // Prepend the appropriate URI scheme header depending // on the MIME type switch mimeType { case "image/jpeg": base64Encoding += "data:image/jpeg;base64," case "image/png": base64Encoding += "data:image/png;base64," } // Append the base64 encoded output base64Encoding += base64.StdEncoding.EncodeToString(bytes) p.ImageBase64 = base64Encoding } w.Header().Set("Content-Type", "application/json") if len(products) != 0 { // Return only the first result for now. Enhance this with a dedicated dialog // displaying all the references found? return json.NewEncoder(w).Encode(products) } return nil } }