Files
heliosian/tools/browse/main.go
T

179 lines
5.0 KiB
Go
Raw Normal View History

// Command browse drives the capture browser one step at a time: act, capture, report.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/chromedp/cdproto/input"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
)
type pageTarget struct {
ID string `json:"id"`
Type string `json:"type"`
URL string `json:"url"`
Title string `json:"title"`
}
func currentTarget() (string, error) {
resp, err := http.Get("http://localhost:9222/json/list")
if err != nil {
return "", fmt.Errorf("capture browser not reachable on localhost:9222, run tools/capturebrowser first: %w", err)
}
defer resp.Body.Close()
targets := []pageTarget{}
if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil {
return "", err
}
for _, t := range targets {
if t.Type != "page" {
continue
}
if strings.HasPrefix(t.URL, "devtools://") || strings.HasPrefix(t.URL, "chrome-extension://") {
continue
}
return t.ID, nil
}
return newTab()
}
func newTab() (string, error) {
req, err := http.NewRequest(http.MethodPut, "http://localhost:9222/json/new?url=about:blank", nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
t := pageTarget{}
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
return "", err
}
if t.ID == "" {
return "", fmt.Errorf("capture browser did not create a tab")
}
return t.ID, nil
}
func parseXY(coords string) (float64, float64, error) {
parts := strings.Split(coords, ",")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("click coordinates must be x,y, got %q", coords)
}
x, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
if err != nil {
return 0, 0, err
}
y, err := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
if err != nil {
return 0, 0, err
}
return x, y, nil
}
func keyChord(name string) string {
switch strings.ToLower(name) {
case "enter":
return "\r"
case "tab":
return "\t"
case "escape":
return ""
case "backspace":
return "\b"
default:
return name
}
}
func main() {
nav := flag.String("nav", "", "navigate to url")
back := flag.Bool("back", false, "navigate back in history")
scroll := flag.Int("scroll", 0, "scroll vertically by pixels, negative scrolls up")
clickSel := flag.String("clicksel", "", "click the first element matching css selector")
click := flag.String("click", "", "click at viewport coordinates x,y as shown in the screenshot")
typeText := flag.String("type", "", "insert text into the focused element")
key := flag.String("key", "", "press a key: enter, tab, escape, backspace, or a literal character")
wait := flag.String("wait", "", "css selector that must be visible before capturing")
dump := flag.Bool("dump", false, "print page html instead of writing a screenshot")
out := flag.String("out", "screenshots/browse.png", "output png path")
flag.Parse()
id, err := currentTarget()
if err != nil {
log.Fatalf("[ERROR] %v", err)
}
allocCtx, _ := chromedp.NewRemoteAllocator(context.Background(), "http://localhost:9222")
// cancelling the chromedp context closes the attached tab; the tab must outlive this process
ctx, _ := chromedp.NewContext(allocCtx, chromedp.WithTargetID(target.ID(id)))
ctx, cancelTimeout := context.WithTimeout(ctx, 15*time.Second)
defer cancelTimeout()
actions := []chromedp.Action{chromedp.EmulateViewport(1280, 800)}
if *nav != "" {
actions = append(actions, chromedp.Navigate(*nav))
}
if *back {
actions = append(actions, chromedp.NavigateBack())
}
if *scroll != 0 {
actions = append(actions, chromedp.Evaluate(fmt.Sprintf("window.scrollBy(0, %d)", *scroll), nil))
}
if *clickSel != "" {
actions = append(actions, chromedp.Click(*clickSel, chromedp.ByQuery))
}
if *click != "" {
x, y, err := parseXY(*click)
if err != nil {
log.Fatalf("[ERROR] %v", err)
}
actions = append(actions, chromedp.MouseClickXY(x, y))
}
if *typeText != "" {
actions = append(actions, input.InsertText(*typeText))
}
if *key != "" {
actions = append(actions, chromedp.KeyEvent(keyChord(*key)))
}
if *wait != "" {
actions = append(actions, chromedp.WaitVisible(*wait, chromedp.ByQuery))
}
actions = append(actions, chromedp.Sleep(700*time.Millisecond))
var html string
var png []byte
if *dump {
actions = append(actions, chromedp.OuterHTML("html", &html, chromedp.ByQuery))
} else {
actions = append(actions, chromedp.CaptureScreenshot(&png))
}
var location, title string
actions = append(actions, chromedp.Location(&location), chromedp.Title(&title))
if err := chromedp.Run(ctx, actions...); err != nil {
log.Fatalf("[ERROR] browse: %v", err)
}
if *dump {
fmt.Println(html)
} else {
if err := os.MkdirAll(filepath.Dir(*out), 0o755); err != nil {
log.Fatalf("[ERROR] create output dir: %v", err)
}
if err := os.WriteFile(*out, png, 0o644); err != nil {
log.Fatalf("[ERROR] write %s: %v", *out, err)
}
}
fmt.Printf("url: %s\ntitle: %s\n", location, title)
}