From 0ed69b75a1fb2e0935a2cf856d61f379a35a1c03 Mon Sep 17 00:00:00 2001 From: Crumb Owl Date: Sun, 6 Jul 2025 20:51:03 +0200 Subject: [PATCH] ProductBarcode: add first backend API implementation --- backend/app/api/handlers/v1/v1_ctrl_qrcode.go | 265 ++++++++++++++++++ backend/app/api/routes.go | 1 + backend/app/api/static/docs/docs.go | 32 +++ backend/app/api/static/docs/swagger.json | 32 +++ backend/app/api/static/docs/swagger.yaml | 19 ++ docs/en/api/openapi-2.0.json | 32 +++ docs/en/api/openapi-2.0.yaml | 19 ++ 7 files changed, 400 insertions(+) diff --git a/backend/app/api/handlers/v1/v1_ctrl_qrcode.go b/backend/app/api/handlers/v1/v1_ctrl_qrcode.go index 333f8d00..59f817e2 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_qrcode.go +++ b/backend/app/api/handlers/v1/v1_ctrl_qrcode.go @@ -2,12 +2,15 @@ package v1 import ( "bytes" + "encoding/json" "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/web/adapters" "github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/writer/standard" @@ -70,3 +73,265 @@ func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc { return qrc.Save(qrwriter) } } + +type BarcodeProduct struct { + SearchEngineName string `json:"search_engine_name"` + + //Name string `json:"name" validate:"required,min=1,max=255"` + //Description string `json:"description" validate:"max=1000"` + + // Identifications + ModelNumber string `json:"modelNumber"` + Manufacturer string `json:"manufacturer"` + + // Extras + Country string `json:"notes"` + Barcode string `json:"barcode"` + + // TODO: add image attachement + // TODO: add asin? + ImageURL string `json:"imageURL"` + + item repo.ItemCreate +} + +/* + 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"` + Currency string `json:"currency"` + 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"` + } `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.ItemCreate +// @Router /v1/getproductfromean [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleProductSearchEAN() 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 []BarcodeProduct + + // www.ean-search.org/: not free + + // Example code: dewalt 5035048748428 + + upcitemdb := func(iEan string) ([]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 []BarcodeProduct + + for _, it := range result.Items { + var p 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: Freetoken: 43866b1fa5d558a2bd12 + barcodespider := func(iEan string) ([]BarcodeProduct, error) { + req, err := http.NewRequest( + "GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil) + + if err != nil { + return nil, err + } + + req.Header.Add("token", "43866b1fa5d558a2bd12") + + 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 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 []BarcodeProduct + res = append(res, p) + + return res, nil + } + + ps2, err := barcodespider(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]) + } + + w.Header().Set("Content-Type", "application/json") + + if len(products) != 0 { + return json.NewEncoder(w).Encode(products[0].item) + } + + return nil + } +} diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 5d7900e4..42796e74 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -158,6 +158,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR } r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...)) + r.Get("/getproductfromean", chain.ToHandlerFunc(v1Ctrl.HandleProductSearchEAN(), userMW...)) r.Get( "/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...), diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 843f8cea..3f2900ee 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -195,6 +195,38 @@ const docTemplate = `{ } } }, + "/v1/getproductfromean": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Search EAN from Barcode", + "parameters": [ + { + "type": "string", + "description": "barcode to be searched", + "name": "data", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 52616edb..e8cb6adf 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -193,6 +193,38 @@ } } }, + "/v1/getproductfromean": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Search EAN from Barcode", + "parameters": [ + { + "type": "string", + "description": "barcode to be searched", + "name": "data", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 35cb6d8c..5bb042dc 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -1551,6 +1551,25 @@ paths: summary: Currency tags: - Base + /v1/getproductfromean: + get: + parameters: + - description: barcode to be searched + in: query + name: data + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + security: + - Bearer: [] + summary: Search EAN from Barcode + tags: + - Items /v1/groups: get: produces: diff --git a/docs/en/api/openapi-2.0.json b/docs/en/api/openapi-2.0.json index 52616edb..e8cb6adf 100644 --- a/docs/en/api/openapi-2.0.json +++ b/docs/en/api/openapi-2.0.json @@ -193,6 +193,38 @@ } } }, + "/v1/getproductfromean": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Search EAN from Barcode", + "parameters": [ + { + "type": "string", + "description": "barcode to be searched", + "name": "data", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ diff --git a/docs/en/api/openapi-2.0.yaml b/docs/en/api/openapi-2.0.yaml index 35cb6d8c..5bb042dc 100644 --- a/docs/en/api/openapi-2.0.yaml +++ b/docs/en/api/openapi-2.0.yaml @@ -1551,6 +1551,25 @@ paths: summary: Currency tags: - Base + /v1/getproductfromean: + get: + parameters: + - description: barcode to be searched + in: query + name: data + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + security: + - Bearer: [] + summary: Search EAN from Barcode + tags: + - Items /v1/groups: get: produces: