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
This commit is contained in:
commit
66001f861f
7 changed files with 293 additions and 0 deletions
29
client/client.go
Normal file
29
client/client.go
Normal file
|
@ -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)
|
||||||
|
}
|
84
crawler/crawler.go
Normal file
84
crawler/crawler.go
Normal file
|
@ -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
|
||||||
|
}
|
1
go.mod
Normal file
1
go.mod
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module github.com/mrahbar/my-bloody-hetzner-sb-notifier
|
12
hetzner/endpoint.go
Normal file
12
hetzner/endpoint.go
Normal file
|
@ -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())
|
||||||
|
}
|
74
hetzner/types.go
Normal file
74
hetzner/types.go
Normal file
|
@ -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)
|
||||||
|
}
|
92
main.go
Normal file
92
main.go
Normal file
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
1
notifier/notifier.go
Normal file
1
notifier/notifier.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package notifier
|
Reference in a new issue