product images added to the catalogue
This commit is contained in:
151
scratch/productimages/main.go
Normal file
151
scratch/productimages/main.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Backfills products.productimages for everything imported before the column
|
||||
// existed.
|
||||
//
|
||||
// The import used to keep `Images[0]` and discard the rest, so roughly six
|
||||
// thousand products carry one photo where the global catalogue has up to ten.
|
||||
// Re-importing would work but rewrites price, category and stock links; this
|
||||
// only fills the gallery.
|
||||
//
|
||||
// The images are re-listed rather than reconstructed. Filenames are
|
||||
// `image_000.jpeg`, `image_001.jpg` … — the extension varies within a single
|
||||
// product, so the sibling URLs cannot be derived from the first one. The
|
||||
// `image_id` is taken out of the stored URL and the same S3 listing the API
|
||||
// uses is asked for the rest, which is why this boots the app's own
|
||||
// connections instead of talking to Postgres alone.
|
||||
//
|
||||
// Dry by default; pass --apply to write.
|
||||
//
|
||||
// go run ./scratch/productimages # report only
|
||||
// go run ./scratch/productimages --apply # write
|
||||
// go run ./scratch/productimages --apply --tenant 1087
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"nearle/db"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type row struct {
|
||||
Productid int
|
||||
Tenantid int
|
||||
Productbrand string
|
||||
Productimage string
|
||||
}
|
||||
|
||||
func main() {
|
||||
apply := flag.Bool("apply", false, "write the changes; otherwise report only")
|
||||
tenant := flag.Int("tenant", 0, "restrict to one tenant")
|
||||
flag.Parse()
|
||||
|
||||
_ = godotenv.Load()
|
||||
db.Connect()
|
||||
|
||||
if db.ImageStore == nil {
|
||||
log.Fatal("the image store is not configured (USE_S3 / S3_*), so there is nothing to list images from")
|
||||
}
|
||||
|
||||
q := `
|
||||
SELECT productid, tenantid,
|
||||
COALESCE(productbrand,'') AS productbrand,
|
||||
COALESCE(productimage,'') AS productimage
|
||||
FROM products
|
||||
WHERE COALESCE(productimage,'') <> ''
|
||||
AND COALESCE(productimages::text,'') IN ('', 'null', '[]')`
|
||||
args := []interface{}{}
|
||||
if *tenant > 0 {
|
||||
q += ` AND tenantid = ?`
|
||||
args = append(args, *tenant)
|
||||
}
|
||||
q += ` ORDER BY productid`
|
||||
|
||||
var rows []row
|
||||
if err := db.DB.Raw(q, args...).Scan(&rows).Error; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("candidates: %d\n\n", len(rows))
|
||||
|
||||
var filled, single, unparsed, nolist int
|
||||
for _, r := range rows {
|
||||
brand, imageID := parseImageRef(r.Productimage)
|
||||
if brand == "" || imageID == "" {
|
||||
unparsed++
|
||||
continue
|
||||
}
|
||||
// The stored brand is authoritative where it exists; the URL is only a
|
||||
// fallback, because a product renamed in the back office keeps its
|
||||
// original image path.
|
||||
if b := strings.ToLower(strings.TrimSpace(r.Productbrand)); b != "" {
|
||||
brand = b
|
||||
}
|
||||
|
||||
images := db.GetImages(brand, imageID)
|
||||
if len(images) == 0 {
|
||||
nolist++
|
||||
continue
|
||||
}
|
||||
if len(images) == 1 {
|
||||
// Nothing gained — leave the column null rather than writing a
|
||||
// one-element array that says the same as productimage.
|
||||
single++
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(images)
|
||||
if err != nil {
|
||||
log.Printf("product %d: encoding failed: %v", r.Productid, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if *apply {
|
||||
if err := db.DB.Exec(
|
||||
`UPDATE products SET productimages = ?::jsonb, updated = NOW() WHERE productid = ?`,
|
||||
string(encoded), r.Productid).Error; err != nil {
|
||||
log.Printf("product %d: update failed: %v", r.Productid, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
filled++
|
||||
if filled <= 10 {
|
||||
fmt.Printf(" product %-7d %-10s %-34s %d images\n", r.Productid, brand, imageID, len(images))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(`
|
||||
would fill (>1 image) : %d
|
||||
only one image : %d
|
||||
image_id unparseable : %d
|
||||
nothing listed in S3 : %d
|
||||
`, filled, single, unparsed, nolist)
|
||||
|
||||
if !*apply {
|
||||
fmt.Println("\ndry run — nothing written. Re-run with --apply.")
|
||||
}
|
||||
}
|
||||
|
||||
// parseImageRef pulls the brand and image_id out of a stored image URL.
|
||||
//
|
||||
// https://…/daily/brands/nestle/nestle_maggi_615a9fa9/image_000.jpeg
|
||||
// ^brand ^image_id
|
||||
//
|
||||
// Returns empty strings for anything that does not match, so a hand-uploaded
|
||||
// image URL is skipped rather than guessed at.
|
||||
func parseImageRef(url string) (brand, imageID string) {
|
||||
const marker = "/daily/brands/"
|
||||
i := strings.Index(url, marker)
|
||||
if i < 0 {
|
||||
return "", ""
|
||||
}
|
||||
parts := strings.Split(strings.Trim(url[i+len(marker):], "/"), "/")
|
||||
if len(parts) < 3 {
|
||||
return "", ""
|
||||
}
|
||||
return strings.ToLower(parts[0]), parts[1]
|
||||
}
|
||||
Reference in New Issue
Block a user