Damnit again
Former-commit-id: b394f26caf0df7d113ac4cc7dacc9c544af6897f
This commit is contained in:
132
copart/auction/.auction.go
Normal file
132
copart/auction/.auction.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"local/sandbox/selenium/copart/copart/browser"
|
||||
"local/sandbox/selenium/copart/copart/config"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/tebeka/selenium"
|
||||
)
|
||||
|
||||
type Auction struct {
|
||||
browser *browser.Browser
|
||||
cars []*Car
|
||||
stop bool
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func New(url string) (*Auction, error) {
|
||||
b, err := browser.New()
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := b.Get(url); err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var elems []selenium.WebElement
|
||||
deadline := time.Now().Add(time.Second * 10) // TODO ctx configurable and load more later
|
||||
for deadline.After(time.Now()) {
|
||||
time.Sleep(time.Second)
|
||||
elems, err = b.Driver.FindElements("xpath", "//iframe")
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
if len(elems) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
b.Close()
|
||||
return nil, b.Close()
|
||||
}
|
||||
url, err = elems[0].GetAttribute("src")
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := b.Get(url); err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Auction{
|
||||
browser: b,
|
||||
cars: []*Car{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Auction) Start() error {
|
||||
log.Println("START", a.stop)
|
||||
go func() {
|
||||
defer func() {
|
||||
a.stopped = true
|
||||
}()
|
||||
for !a.stop {
|
||||
log.Println("TOP")
|
||||
c := NewCar()
|
||||
if err := c.Parse(a.browser.Driver); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
log.Println("C", c)
|
||||
for !a.stop {
|
||||
time.Sleep(time.Second)
|
||||
d := NewCar()
|
||||
if err := d.Parse(a.browser.Driver); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
log.Printf("%v = (%v == %v)", c.Equals(d), c.String(), d.String())
|
||||
if !c.Equals(d) {
|
||||
break
|
||||
}
|
||||
c = d
|
||||
}
|
||||
b, err := c.Encode()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if err := config.Values().DB.Set(c.String(), b); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
var list []string
|
||||
if b, err := config.Values().DB.Get("LIST"); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
} else if err := json.Unmarshal(b, &list); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
} else {
|
||||
list = append(list, c.String())
|
||||
b, _ := json.Marshal(list)
|
||||
if err := config.Values().DB.Set("LIST", b); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auction) Stop() error {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
a.stop = true
|
||||
for !a.stopped {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if a.browser == nil || a.browser.Driver == nil {
|
||||
return nil
|
||||
}
|
||||
return a.browser.Driver.Close()
|
||||
}
|
||||
222
copart/auction/auction.go
Normal file
222
copart/auction/auction.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"local/sandbox/selenium/copart/copart/browser"
|
||||
"local/sandbox/selenium/copart/copart/config"
|
||||
"local/storage"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Auction struct {
|
||||
browser *browser.Browser
|
||||
cars chan *Car
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
routines chan struct{}
|
||||
}
|
||||
|
||||
func New(b *browser.Browser, url string) (*Auction, error) {
|
||||
if err := b.Get(url); err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer can()
|
||||
|
||||
var elem WebElement
|
||||
for err := ctx.Err(); err == nil; err = ctx.Err() {
|
||||
elem, err = b.Driver.FindElement("xpath", "//iframe")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iframeURL, err := elem.GetAttribute("src")
|
||||
if err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
} else if iframeURL == "" {
|
||||
b.Close()
|
||||
return nil, errors.New("auction iframe has no src")
|
||||
}
|
||||
|
||||
if err := b.Get(iframeURL); err != nil {
|
||||
b.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, can = context.WithCancel(context.Background())
|
||||
return &Auction{
|
||||
browser: b,
|
||||
cars: make(chan *Car),
|
||||
ctx: ctx,
|
||||
can: can,
|
||||
routines: make(chan struct{}, 5),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Auction) Start() error {
|
||||
go a.parseCars()
|
||||
a.routines <- struct{}{}
|
||||
go a.saveCars()
|
||||
a.routines <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auction) parseCars() error {
|
||||
log.Printf("[parseCars] starting")
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("[parseCars] recover:", err)
|
||||
}
|
||||
log.Printf("[parseCars] stopping")
|
||||
close(a.cars)
|
||||
a.can()
|
||||
a.routines <- struct{}{}
|
||||
}()
|
||||
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
|
||||
c, err := a.nextCar()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("[parseCars] found car %v", c.String())
|
||||
if !strings.Contains(c.Bid, "$") {
|
||||
continue
|
||||
}
|
||||
if b, _ := c.MarshalJSON(); strings.Contains(strings.ToLower(string(b)), ", hi") {
|
||||
continue
|
||||
} else if strings.Contains(strings.ToLower(string(b)), "guam") {
|
||||
continue
|
||||
} else if len(c.Title) < 4 {
|
||||
continue
|
||||
} else if v, err := strconv.Atoi(c.Title[:4]); err != nil {
|
||||
continue
|
||||
} else if v+5 < time.Now().Year() {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case a.cars <- c:
|
||||
case <-a.ctx.Done():
|
||||
panic(a.ctx.Err())
|
||||
}
|
||||
}
|
||||
return a.ctx.Err()
|
||||
}
|
||||
|
||||
func (a *Auction) nextCar() (*Car, error) {
|
||||
c := NewCar()
|
||||
if err := c.Parse(a.browser.Driver); err != nil {
|
||||
if !strings.Contains(err.Error(), "stale element") {
|
||||
return nil, err
|
||||
} else {
|
||||
return a.nextCar()
|
||||
}
|
||||
}
|
||||
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
|
||||
time.Sleep(time.Second * 2)
|
||||
d := NewCar()
|
||||
if err := d.Parse(a.browser.Driver); err != nil {
|
||||
if !strings.Contains(err.Error(), "stale element") {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !c.Equals(d) {
|
||||
return c, nil
|
||||
}
|
||||
if d.Bid != "" {
|
||||
c = d
|
||||
}
|
||||
}
|
||||
return nil, a.ctx.Err()
|
||||
}
|
||||
|
||||
func (a *Auction) saveCars() error {
|
||||
log.Printf("[saveCars] starting")
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("[saveCars] recover:", err)
|
||||
}
|
||||
log.Printf("[saveCars] stopping")
|
||||
a.can()
|
||||
a.routines <- struct{}{}
|
||||
}()
|
||||
for err := a.ctx.Err(); err == nil; err = a.ctx.Err() {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
panic(a.ctx.Err())
|
||||
case c := <-a.cars:
|
||||
go func(d *Car) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[saveCars] saving %v", d.String())
|
||||
if err := a.saveCar(d); err != nil {
|
||||
log.Printf("[saveCars] err: %v", err)
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
return a.ctx.Err()
|
||||
}
|
||||
|
||||
func (a *Auction) saveCar(c *Car) error {
|
||||
db := config.Values().DB
|
||||
|
||||
b, err := c.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Set(c.String(), b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var list []string
|
||||
b, err = db.Get("LIST")
|
||||
if err == storage.ErrNotFound {
|
||||
b = []byte("[]")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, &list); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list = append([]string{c.String()}, list...)
|
||||
if b, err := json.Marshal(list); err != nil {
|
||||
return err
|
||||
} else if err := db.Set("LIST", b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Auction) Stop() error {
|
||||
l := len(a.routines)
|
||||
a.can()
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer can()
|
||||
for i := 0; i < 2*l; i++ {
|
||||
select {
|
||||
case <-a.routines:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
if len(a.routines) > 0 {
|
||||
return errors.New("not all routines exited")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
185
copart/auction/auction_test.go
Normal file
185
copart/auction/auction_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"local/sandbox/selenium/copart/copart/browser"
|
||||
"local/sandbox/selenium/copart/copart/config"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func randStr() string {
|
||||
n := 3
|
||||
letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
k := big.NewInt(int64(len(letters)))
|
||||
b := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
j, _ := rand.Int(rand.Reader, k)
|
||||
b[i] = letters[int(j.Int64())]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func randomAuctionBody() []browser.MockElement {
|
||||
return []browser.MockElement{browser.MockElement{
|
||||
Attrs: map[string]string{
|
||||
"src": "anything",
|
||||
},
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"//bidding-dialer-refactor": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"text": []browser.MockElement{browser.MockElement{
|
||||
Txt: "$bid" + randStr(),
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"//lot-details-primary-refactored": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"label": []browser.MockElement{browser.MockElement{
|
||||
Txt: "label" + randStr(),
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"../div[1]": []browser.MockElement{browser.MockElement{
|
||||
Txt: "value" + randStr(),
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"//lot-details-secondary-refactored": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"label": []browser.MockElement{browser.MockElement{
|
||||
Txt: "label" + randStr(),
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"../div[1]": []browser.MockElement{browser.MockElement{
|
||||
Txt: "value" + randStr(),
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"//ngx-carousel": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"ngx-item": []browser.MockElement{},
|
||||
},
|
||||
}},
|
||||
"//lot-header": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"section": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"div": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
"div": []browser.MockElement{browser.MockElement{
|
||||
Txt: "title" + randStr(),
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"//widget-header-sale": []browser.MockElement{browser.MockElement{
|
||||
Ret: map[string][]browser.MockElement{
|
||||
".//div[@class=\"watchlist\"]": []browser.MockElement{},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func mockAuction() (*Auction, context.CancelFunc) {
|
||||
m := &sync.Map{}
|
||||
r := randomAuctionBody()
|
||||
m.Store("body", r)
|
||||
go func() {
|
||||
time.Sleep(time.Second * 2)
|
||||
m.Store("//iframe", r)
|
||||
}()
|
||||
driver := browser.NewMockDriver(m)
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
for ctx.Err() == nil {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
m.Store("body", randomAuctionBody())
|
||||
}
|
||||
}()
|
||||
return &Auction{
|
||||
browser: &browser.Browser{Driver: driver},
|
||||
cars: make(chan *Car),
|
||||
ctx: ctx,
|
||||
can: can,
|
||||
routines: make(chan struct{}, 5),
|
||||
}, can
|
||||
}
|
||||
|
||||
func TestNewAuction(t *testing.T) {
|
||||
a, can := mockAuction()
|
||||
defer can()
|
||||
b := a.browser
|
||||
|
||||
if _, err := New(b, "any"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuctionStartStop(t *testing.T) {
|
||||
a, can := mockAuction()
|
||||
defer can()
|
||||
if err := a.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(a.routines) != 2 {
|
||||
t.Fatal("not 2 routines")
|
||||
}
|
||||
if err := a.Stop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuctionSaveCar(t *testing.T) {
|
||||
os.Args = []string{"a", "-db", "map"}
|
||||
config.New()
|
||||
a, can := mockAuction()
|
||||
defer can()
|
||||
c := NewCar()
|
||||
c.Bid = "123"
|
||||
c.Title = "abc"
|
||||
c.Details = map[string]string{
|
||||
"x": "y",
|
||||
}
|
||||
c.Images = []*Image{&Image{Src: &url.URL{Host: "img"}}}
|
||||
d := NewCar()
|
||||
if err := a.saveCar(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b, err := config.Values().DB.Get("LIST"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !strings.Contains(string(b), c.String()) {
|
||||
t.Fatal("list does not contain new car")
|
||||
}
|
||||
if b, err := config.Values().DB.Get(c.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := d.Decode(b); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !c.Equals(d) {
|
||||
t.Fatal("saved car != fetched car")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuctionNextCar(t *testing.T) {
|
||||
os.Args = []string{"a", "-db", "map"}
|
||||
config.New()
|
||||
a, can := mockAuction()
|
||||
defer can()
|
||||
if c, err := a.nextCar(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if c == nil || fmt.Sprintf("%v", *c) == fmt.Sprintf("%v", Car{}) {
|
||||
t.Fatal("parsed car is nil")
|
||||
}
|
||||
}
|
||||
251
copart/auction/car.go
Normal file
251
copart/auction/car.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Car struct {
|
||||
Bid string
|
||||
Details map[string]string
|
||||
Images []*Image
|
||||
Title string
|
||||
Watched bool
|
||||
TS time.Time
|
||||
}
|
||||
|
||||
type CarOption func(*Car)
|
||||
|
||||
func NewCar(options ...CarOption) *Car {
|
||||
c := &Car{
|
||||
Details: map[string]string{},
|
||||
Images: []*Image{},
|
||||
}
|
||||
c.parseTS(nil)
|
||||
for _, opt := range options {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Car) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]interface{}{}
|
||||
m["bid"] = c.Bid
|
||||
m["details"] = c.Details
|
||||
m["images"] = c.Images
|
||||
m["title"] = c.Title
|
||||
m["TS"] = c.TS.Format("2006-01-02")
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (c *Car) Encode() ([]byte, error) {
|
||||
buff := bytes.NewBuffer(nil)
|
||||
enc := gob.NewEncoder(buff)
|
||||
err := enc.Encode(c)
|
||||
return buff.Bytes(), err
|
||||
}
|
||||
|
||||
func (c *Car) Decode(b []byte) error {
|
||||
buff := bytes.NewBuffer(b)
|
||||
enc := gob.NewDecoder(buff)
|
||||
return enc.Decode(c)
|
||||
}
|
||||
|
||||
func (c *Car) Copy() *Car {
|
||||
d := *c
|
||||
return &d
|
||||
}
|
||||
|
||||
func (c *Car) Equals(d *Car) bool {
|
||||
if c == d {
|
||||
return true
|
||||
}
|
||||
if d == nil && c != nil {
|
||||
return false
|
||||
}
|
||||
if c == nil && d != nil {
|
||||
return false
|
||||
}
|
||||
for k, v := range c.Details {
|
||||
if w, ok := d.Details[k]; ok && w != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for k, v := range c.Details {
|
||||
if w, ok := c.Details[k]; ok && w != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if c.Title != d.Title {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Car) String() string {
|
||||
return fmt.Sprintf(
|
||||
"[bid:%v title:%v watched:%v images:%v when:%v]",
|
||||
c.Bid,
|
||||
c.Title,
|
||||
c.Watched,
|
||||
len(c.Images),
|
||||
c.TS,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Car) Parse(wd WebDriver) error {
|
||||
we, err := wd.FindElement("tag name", "body")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
do := []func(WebElement) error{
|
||||
c.parseTitle,
|
||||
c.parseBid,
|
||||
c.parseWatched,
|
||||
c.parseImages,
|
||||
c.parseDetails,
|
||||
c.parseTS,
|
||||
}
|
||||
for _, d := range do {
|
||||
if err := d(we); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseTS(WebElement) error {
|
||||
c.TS = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseBid(wd WebElement) error {
|
||||
elem := waitForXPath(wd, "//bidding-dialer-refactor")
|
||||
|
||||
elem, err := elem.FindElement("tag name", "text")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := elem.Text()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(text, "$") {
|
||||
c.Bid = text
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseDetails(wd WebElement) error {
|
||||
details := make(map[string]string)
|
||||
|
||||
elems, err := wd.FindElements("tag name", "perfect-scrollbar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(elems) > 2 {
|
||||
elems = elems[:2]
|
||||
}
|
||||
|
||||
for _, elem := range elems {
|
||||
labels, err := elem.FindElements("tag name", "label")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, label := range labels {
|
||||
ltext, err := label.Text()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ltext == "" {
|
||||
continue
|
||||
}
|
||||
value, err := label.FindElement("xpath", "../div[1]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vtext, err := value.Text()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
details[ltext] = vtext
|
||||
}
|
||||
}
|
||||
|
||||
if len(details) > 0 {
|
||||
c.Details = details
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseImages(wd WebElement) error {
|
||||
elem := waitForXPath(wd, "//ngx-carousel")
|
||||
|
||||
elems, err := elem.FindElements("tag name", "ngx-item")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(elems) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
images := make([]*Image, 0)
|
||||
for _, elem := range elems {
|
||||
elems, err := elem.FindElements("tag name", "img")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, img := range elems {
|
||||
attr, err := img.GetAttribute("src")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := url.Parse(attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images = append(images, NewImage(url))
|
||||
}
|
||||
}
|
||||
|
||||
c.Images = images
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseTitle(wd WebElement) error {
|
||||
elem := waitForXPath(wd, "//lot-header")
|
||||
|
||||
elem = followTags(elem, "section", "div", "div")
|
||||
if elem == nil {
|
||||
return errors.New("not found")
|
||||
}
|
||||
|
||||
text, err := elem.Text()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Title = text
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Car) parseWatched(wd WebElement) error {
|
||||
elem := waitForXPath(wd, "//widget-header-sale")
|
||||
|
||||
elems, err := elem.FindElements("tag name", ".//div[@class=\"watchlist\"]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Watched = len(elems) > 0
|
||||
return nil
|
||||
}
|
||||
36
copart/auction/car_test.go
Normal file
36
copart/auction/car_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package auction
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCar(t *testing.T) {
|
||||
c := NewCar()
|
||||
c.Title = "title"
|
||||
c.Bid = "bid"
|
||||
c.Watched = true
|
||||
c.Details = map[string]string{"a": "b"}
|
||||
d := c.Copy()
|
||||
if !c.Equals(d) {
|
||||
t.Errorf("equals failed post copy")
|
||||
}
|
||||
c.Details = map[string]string{"a": "c"}
|
||||
if c.Equals(d) {
|
||||
t.Errorf("equals passes post edit")
|
||||
}
|
||||
t.Log(c.String())
|
||||
|
||||
b, err := c.Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e := NewCar()
|
||||
if err := e.Decode(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !c.Equals(e) {
|
||||
t.Fatalf("decoded != encoded: %v vs %v", c, e)
|
||||
}
|
||||
|
||||
if _, err := c.MarshalJSON(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
39
copart/auction/image.go
Normal file
39
copart/auction/image.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Raw []byte
|
||||
Src *url.URL
|
||||
}
|
||||
|
||||
func NewImage(url *url.URL) *Image {
|
||||
return &Image{
|
||||
Src: url,
|
||||
}
|
||||
}
|
||||
|
||||
func (img *Image) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(img.Src.String())
|
||||
}
|
||||
|
||||
func (img *Image) Fetch() error {
|
||||
resp, err := http.Get(img.Src.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
Raw, err := ioutil.ReadAll(resp.Body)
|
||||
img.Raw = Raw
|
||||
return err
|
||||
}
|
||||
|
||||
func (img *Image) Save(path string) error {
|
||||
return ioutil.WriteFile(path, img.Raw, os.ModePerm)
|
||||
}
|
||||
231
copart/auction/today.go
Normal file
231
copart/auction/today.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"local/sandbox/selenium/copart/copart/browser"
|
||||
"local/sandbox/selenium/copart/copart/config"
|
||||
"local/storage"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Today struct {
|
||||
today *browser.Browser
|
||||
Auctions []*Auction
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
routines chan struct{}
|
||||
sigc chan os.Signal
|
||||
}
|
||||
|
||||
func NewToday(sigc chan os.Signal) (*Today, error) {
|
||||
today, err := today()
|
||||
if err != nil {
|
||||
today.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := config.Values().DB.Get("LIST"); err == storage.ErrNotFound {
|
||||
if err := config.Values().DB.Set("LIST", []byte("[]")); err != nil {
|
||||
today.Close()
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
today.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline := time.Now()
|
||||
deadline = deadline.Add(time.Hour * 24)
|
||||
deadline = deadline.Add(time.Duration(-1*time.Now().Hour()) * time.Hour)
|
||||
ctx, can := context.WithDeadline(context.Background(), deadline)
|
||||
return &Today{
|
||||
today: today,
|
||||
Auctions: make([]*Auction, 0),
|
||||
ctx: ctx,
|
||||
can: can,
|
||||
routines: make(chan struct{}, 10),
|
||||
sigc: sigc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func today() (*browser.Browser, error) {
|
||||
today, err := browser.New()
|
||||
if err != nil {
|
||||
return today, err
|
||||
}
|
||||
if err := today.Get("https://www.copart.com/todaysAuction/"); err != nil {
|
||||
return today, err
|
||||
}
|
||||
return today, nil
|
||||
}
|
||||
|
||||
func (t *Today) Start() error {
|
||||
ch := make(chan string)
|
||||
go t.watchAuctionList(ch)
|
||||
t.routines <- struct{}{}
|
||||
go t.startAuctions(ch)
|
||||
t.routines <- struct{}{}
|
||||
return t.ctx.Err()
|
||||
}
|
||||
|
||||
func (t *Today) watchAuctionList(ch chan string) {
|
||||
log.Printf("[watchAuctionList] starting")
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("[watchAuctionList] recover:", err)
|
||||
}
|
||||
log.Printf("[watchAuctionList] stopping")
|
||||
t.routines <- struct{}{}
|
||||
t.Stop()
|
||||
}()
|
||||
found := make(map[string]struct{})
|
||||
retryCD := 3
|
||||
for err := t.ctx.Err(); err == nil; err = t.ctx.Err() {
|
||||
elems, err := t.today.Driver.FindElements("link text", "Join Auction")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("[watchAuctionList] found %v", len(elems))
|
||||
|
||||
for _, elem := range elems {
|
||||
url, err := elem.GetAttribute("href")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
key := time.Now().Format("2006-01-02") + url
|
||||
if _, ok := found[key]; ok {
|
||||
log.Printf("[watchAuctionList] skipping %v", url)
|
||||
continue
|
||||
}
|
||||
found[key] = struct{}{}
|
||||
log.Printf("[watchAuctionList] sending %v", len(url))
|
||||
ch <- url
|
||||
}
|
||||
|
||||
log.Printf("[watchAuctionList] blocking 5 minutes")
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
panic(t.ctx.Err())
|
||||
case <-time.After(time.Minute * 5):
|
||||
retryCD -= 1
|
||||
if retryCD == 0 {
|
||||
retryCD = 3
|
||||
for {
|
||||
if t.ctx.Err() != nil {
|
||||
panic(t.ctx.Err())
|
||||
}
|
||||
err := t.today.Driver.Refresh()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if strings.Contains(err.Error(), "timeout") {
|
||||
select {
|
||||
case <-time.After(time.Second * 15):
|
||||
case <-t.ctx.Done():
|
||||
panic(t.ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Today) startAuctions(ch chan string) {
|
||||
log.Printf("[startAuctions] starting")
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("[startAuctions] recover:", err)
|
||||
}
|
||||
log.Printf("[startAuctions] stopping")
|
||||
t.routines <- struct{}{}
|
||||
t.Stop()
|
||||
}()
|
||||
limiter := rate.NewLimiter(rate.Every(time.Second*15), 1)
|
||||
for {
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
panic(t.ctx.Err())
|
||||
case url := <-ch:
|
||||
if err := limiter.Wait(t.ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("[startAuctions] spawning %v", url)
|
||||
b, err := browser.New()
|
||||
if err != nil {
|
||||
time.Sleep(time.Second * 15)
|
||||
if err := limiter.Wait(t.ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err = browser.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
a, err := New(b, url)
|
||||
if a != nil {
|
||||
t.Auctions = append(t.Auctions, a)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("[startAuctions] failed to New:", err)
|
||||
} else if err := a.Start(); err != nil {
|
||||
log.Println("[startAuctions] failed to New().Start:", err)
|
||||
} else {
|
||||
log.Printf("[startAuctions] spawned %v", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Today) Stop() error {
|
||||
last := make(chan error)
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
l := len(t.routines)
|
||||
t.can()
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer can()
|
||||
for i := 0; i < l*2; i++ {
|
||||
select {
|
||||
case <-t.routines:
|
||||
case <-ctx.Done():
|
||||
select {
|
||||
case last <- ctx.Err():
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range t.Auctions {
|
||||
go func() {
|
||||
if err := a.Stop(); err != nil {
|
||||
select {
|
||||
case last <- err:
|
||||
case <-time.After(time.Second * 5):
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err := t.today.Driver.Close(); err != nil {
|
||||
select {
|
||||
case last <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
var err error
|
||||
select {
|
||||
case err = <-last:
|
||||
case <-time.After(time.Second * 5):
|
||||
}
|
||||
select {
|
||||
case t.sigc <- syscall.SIGHUP:
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
46
copart/auction/webdriver.go
Normal file
46
copart/auction/webdriver.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"local/sandbox/selenium/copart/copart/browser"
|
||||
"time"
|
||||
|
||||
"github.com/tebeka/selenium"
|
||||
)
|
||||
|
||||
type WebDriver interface {
|
||||
browser.WebDriver
|
||||
}
|
||||
|
||||
type WebElement interface {
|
||||
selenium.WebElement
|
||||
}
|
||||
|
||||
func waitForXPath(wd WebElement, path string) selenium.WebElement {
|
||||
var we selenium.WebElement
|
||||
test := func() bool {
|
||||
v, err := wd.FindElements("xpath", path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(v) < 1 {
|
||||
return false
|
||||
}
|
||||
we = v[0]
|
||||
return true
|
||||
}
|
||||
for !test() {
|
||||
time.Sleep(1)
|
||||
}
|
||||
return we
|
||||
}
|
||||
|
||||
func followTags(wd WebElement, tag ...string) selenium.WebElement {
|
||||
for _, t := range tag {
|
||||
var err error
|
||||
wd, err = wd.FindElement("tag name", t)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return wd
|
||||
}
|
||||
Reference in New Issue
Block a user