package main import ( "encoding/json" "fmt" "os" "path/filepath" ) // BundleEntry is one tab in a bundle JSON file. type BundleEntry struct { URL string `json:"url"` Title string `json:"title"` Icon string `json:"icon"` IconW int `json:"icon_w,omitempty"` IconH int `json:"icon_h,omitempty"` IframeOk bool `json:"iframe_ok"` } // Bundle is the top-level JSON structure. type Bundle struct { Entries []BundleEntry `json:"entries"` } // buildEntry creates a BundleEntry for a host, converting its icon if available. func buildEntry(host HostRow, iconsDir string, logWriter *LogWriter, stats *Stats) BundleEntry { entry := BundleEntry{ URL: host.Protocol + "://" + host.Hostname, Title: host.HtmlTitle, Icon: "", IframeOk: host.IframeAllowed, } if host.BestIconHash == "" { return entry } encoded, w, h, convertErr := safeConvert(host.BestIconHash, iconsDir) if convertErr != "" { stats.ConvertErrors.Add(1) logLine := fmt.Sprintf("CONVERT_ERROR: %s %s", host.Hostname, convertErr) fmt.Println(logLine) if logWriter != nil { logWriter.Write(logLine, true) } return entry } entry.Icon = encoded entry.IconW = w entry.IconH = h return entry } // safeConvert wraps convertIconToBase64PNG with panic recovery. func safeConvert(hash, iconsDir string) (encoded string, w, h int, errMsg string) { defer func() { if r := recover(); r != nil { errMsg = fmt.Sprintf("panic: %v", r) } }() var err error encoded, w, h, err = convertIconToBase64PNG(hash, iconsDir) if err != nil { return "", 0, 0, err.Error() } return encoded, w, h, "" } func serializeBundle(entries []BundleEntry) ([]byte, error) { bundle := Bundle{Entries: entries} return json.Marshal(bundle) } func writeBundleLocal(outputDir string, index int, data []byte) error { path := filepath.Join(outputDir, fmt.Sprintf("%06d.json", index)) return os.WriteFile(path, data, 0644) } func writeBundleS3(bucket string, index int, data []byte) error { key := fmt.Sprintf("tabs/%06d.json", index) return s3UploadBundle(bucket, key, data) }