commit 66001f861fd07f981905e5dffffefe05c75bd1b8 Author: Mahmoud Rahbar Azad Date: Mon Oct 22 23:55:29 2018 +0200 Initial commit. Example output: Got 580 offers. Filtered offers: 3 ID| Ram| HDD| CPU| Price| Score| Reduce time|Specials SB57-931121| 128 GB| 2x 2 TB (4096)| Intel Xeon E5-1650V2 (12518)| 57.00 €| 103.12| 53h 53m|ECC, Ent. HDD, iNIC SB69-927780| 128 GB| 2x 2 TB (4096)| Intel Xeon E5-1650V3 (13335)| 69.00 €| 89.92| 49h 39m|ECC, Ent. HDD, iNIC SB76-910394| 128 GB| 3x 2 TB (6144)| Intel Xeon E5-1650V2 (12518)| 76.00 €| 82.73| 01h 21m|ECC, Ent. HDD, iNIC diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..46b22e4 --- /dev/null +++ b/client/client.go @@ -0,0 +1,29 @@ +package client + +import ( + "encoding/json" + "net/http" + "time" +) + +type Client struct { + httpClient *http.Client +} + +func NewClient() *Client { + crawler := &Client{ + &http.Client{Timeout: 10 * time.Second} , + } + + return crawler +} + +func (c *Client) DoRequest(url string, target interface{}) error { + r, err := c.httpClient.Get(url) + if err != nil { + return err + } + defer r.Body.Close() + + return json.NewDecoder(r.Body).Decode(target) +} diff --git a/crawler/crawler.go b/crawler/crawler.go new file mode 100644 index 0000000..6ffe9e4 --- /dev/null +++ b/crawler/crawler.go @@ -0,0 +1,84 @@ +package crawler + +import ( + "fmt" + "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner" + "os" + "sort" + "text/tabwriter" +) + +type Crawler struct { + tabWriter *tabwriter.Writer + + minPrice float64 + maxPrice float64 + + minRam int + maxRam int + + minHddSize int + maxHddSize int + + minHddCount int + maxHddCount int + + minBenchmark int + maxBenchmark int +} + +func NewCrawler(minPrice float64, maxPrice float64, minRam int, maxRam int, minHddSize int, maxHddSize int, minHddCount int, maxHddCount int, minBenchmark int, maxBenchmark int) *Crawler { + crawler := &Crawler{ + tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', tabwriter.Debug|tabwriter.AlignRight), + minPrice, + maxPrice, + minRam, + maxRam, + minHddSize, + maxHddSize, + minHddCount, + maxHddCount, + minBenchmark, + maxBenchmark, + } + + return crawler +} + +func (c *Crawler) Filter(servers []hetzner.Server) []hetzner.Server { + var filteredServers []hetzner.Server + for _, server := range servers { + if !c.isFiltered(server) { + filteredServers = append(filteredServers, server) + } + } + + sort.Slice(servers, func(i, j int) bool { + return servers[i].Score() > servers[j].Score() + }) + return filteredServers +} + +func (c *Crawler) Print(servers []hetzner.Server) { + fmt.Fprintf(c.tabWriter, "%s\n", servers[0].Header()) + for _, server := range servers { + fmt.Fprintf(c.tabWriter, "%s\n", server.ToString()) + } + c.tabWriter.Flush() +} + + +func (c *Crawler) isFiltered(server hetzner.Server) bool { + filtered := true + + priceParsed := server.ParsePrice() + if server.Cpu_benchmark >= c.minBenchmark && server.Cpu_benchmark <= c.maxBenchmark && + priceParsed >= c.minPrice && priceParsed <= c.maxPrice && + server.Ram >= c.minRam && server.Ram <= c.maxRam && + server.TotalHdd() >= c.minHddSize && server.TotalHdd() <= c.maxHddSize && + server.Hdd_count >= c.minHddCount && server.Hdd_count <= c.maxHddCount { + filtered = false + } + + return filtered +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec28d39 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/mrahbar/my-bloody-hetzner-sb-notifier diff --git a/hetzner/endpoint.go b/hetzner/endpoint.go new file mode 100644 index 0000000..b332aae --- /dev/null +++ b/hetzner/endpoint.go @@ -0,0 +1,12 @@ +package hetzner + +import ( + "fmt" + "time" +) + +const hetznerlivedataurl = "https://www.hetzner.de/a_hz_serverboerse/live_data.json" + +func MakeUrl() string { + return fmt.Sprintf("%s?m=%v", hetznerlivedataurl, time.Now().UnixNano()) +} diff --git a/hetzner/types.go b/hetzner/types.go new file mode 100644 index 0000000..15f6c24 --- /dev/null +++ b/hetzner/types.go @@ -0,0 +1,74 @@ +package hetzner + +import ( + "fmt" + "strconv" + "strings" +) + +type Offers struct { + Server []Server `json:"server"` +} + +type Server struct { + Key int `json:"key"` + Name string `json:"name"` + Freetext string `json:"freetext"` + Description []string `json:"description"` + Dist []string `json:"dist"` + Datacenter []string `json:"datacenter"` + Specials []string `json:"specials"` + Traffic string `json:"traffic"` + Bandwith int `json:"bandwith"` + + Price string `json:"price"` + Price_v string `json:"price_v"` + Setup_price string `json:"setup_price"` + + Cpu string `json:"cpu"` + Cpu_benchmark int `json:"cpu_benchmark"` + Cpu_count int `json:"cpu_count"` + Ram int `json:"ram"` + Ram_hr string `json:"ram_hr"` + Hdd_size int `json:"hdd_size"` + Hdd_hr string `json:"hdd_hr"` + Hdd_count int `json:"hdd_count"` + SpecialHdd string `json:"specialHdd"` + + Next_reduce int `json:"next_reduce"` + Next_reduce_hr string `json:"next_reduce_hr"` + + Fixed_price bool `json:"fixed_price"` + Is_highio bool `json:"is_highio"` + Is_ecc bool `json:"is_ecc"` +} + +func (s *Server) TotalHdd() int { + return s.Hdd_count*s.Hdd_size +} + +func (s *Server) Score() float64 { + return (float64(s.TotalHdd())*0.2 + float64(s.Ram)*0.4 + float64(s.Cpu_benchmark)*0.4)/s.ParsePrice() +} + +func (s *Server) ParsePrice() float64 { + priceParsed, err := strconv.ParseFloat(s.Price, 32) + if err != nil { + fmt.Printf("Could not parse price %s for server %s: %s", s.Price, s.Key, err) + return -1 + } + return priceParsed*1.19 +} + +func (s *Server) Header() string { + return fmt.Sprint("ID\tRam\tHDD\tCPU\tPrice\tScore\tReduce time\tSpecials") +} + +func (s *Server) ToString() string { + fixedPriceSymbol := "*" + if !s.Fixed_price { + fixedPriceSymbol = "" + } + specials := strings.Join(s.Specials, ", ") + return fmt.Sprintf("%s-%d\t%s\t%s (%d)\t%s (%d)\t%.2f €%s\t%.2f\t%s\t%s", s.Name, s.Key, s.Ram_hr, s.Hdd_hr, s.TotalHdd(), s.Cpu, s.Cpu_benchmark, s.ParsePrice(), fixedPriceSymbol, s.Score(), s.Next_reduce_hr, specials) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c3a64d0 --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "flag" + "fmt" + "github.com/mrahbar/my-bloody-hetzner-sb-notifier/client" + c "github.com/mrahbar/my-bloody-hetzner-sb-notifier/crawler" + "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner" +) + +var ( + minPrice = flag.Float64( + "min-price", + 0, + "set min price", + ) + maxPrice = flag.Float64( + "max-price", + 297, + "set max price", + ) + + minRam = flag.Int( + "min-ram", + 0, + "set min ram", + ) + maxRam = flag.Int( + "max-ram", + 256, + "set max ram", + ) + + minHddSize = flag.Int( + "min-hdd-size", + 0, + "set min hdd size", + ) + maxHddSize = flag.Int( + "max-hdd-size", + 6144, + "set max hdd size", + ) + + minHddCount = flag.Int( + "min-hdd-count", + 0, + "set min hdd count", + ) + maxHddCount = flag.Int( + "max-hdd-count", + 15, + "set max hdd count", + ) + + minBenchmark = flag.Int( + "min-benchmark", + 0, + "set min benchmark", + ) + maxBenchmark = flag.Int( + "max-benchmark", + 20000, + "set max benchmark", + ) + + alertOnScore = flag.Int( + "alert-on-score", + 0, + "set alert on score", + ) +) + +func main() { + flag.Parse() + + offers := &hetzner.Offers{} + err := client.NewClient().DoRequest(hetzner.MakeUrl(), offers) + if err != nil { + panic(fmt.Errorf("failed to get hetzner live data: %s", err)) + } + + if len(offers.Server) > 0 { + crawler := c.NewCrawler(*minPrice, *maxPrice, *minRam, *maxRam, *minHddSize, *maxHddSize, *minHddCount, *maxHddCount, *minBenchmark, *maxBenchmark) + servers := crawler.Filter(offers.Server) + + fmt.Printf("Got %d offers. Filtered offers: %d\n", len(offers.Server), len(servers)) + crawler.Print(servers) + } else { + fmt.Println("Got no offers.") + } +} diff --git a/notifier/notifier.go b/notifier/notifier.go new file mode 100644 index 0000000..ed45f23 --- /dev/null +++ b/notifier/notifier.go @@ -0,0 +1 @@ +package notifier