├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod └── main.go /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - goos: 3 | - darwin 4 | - linux 5 | - freebsd 6 | - openbsd 7 | - netbsd 8 | - windows 9 | goarch: 10 | - amd64 11 | - arm64 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ajin Asokan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CLI for fast.com written in Go 2 | 3 | Download binary from [releases](https://github.com/ajinasokan/fast/releases) or build manually: 4 | 5 | ```sh 6 | $ go install github.com/ajinasokan/fast@latest 7 | 8 | $ fast 9 | 10 | 2023/04/27 20:29:53 Preparing 11 | 2023/04/27 20:29:55 Downloading https://... 12 | 2023/04/27 20:29:55 Downloading https://... 13 | 2023/04/27 20:29:55 Downloading https://... 14 | 2023/04/27 20:29:55 Downloading https://... 15 | 2023/04/27 20:29:55 Downloading https://... 16 | 2023/04/27 20:29:55 Speed 166.8 Mbps 17 | 2023/04/27 20:29:56 Speed 286.4 Mbps 18 | 2023/04/27 20:29:56 Speed 151.9 Mbps 19 | 2023/04/27 20:29:57 Speed 187.1 Mbps 20 | 2023/04/27 20:29:57 Speed 152.6 Mbps 21 | 2023/04/27 20:29:58 Speed 193.2 Mbps 22 | 2023/04/27 20:29:58 Speed 203.7 Mbps 23 | 2023/04/27 20:29:59 Speed 54.9 Mbps 24 | 2023/04/27 20:29:59 Speed 76.7 Mbps 25 | 2023/04/27 20:30:00 Speed 104.2 Mbps 26 | 2023/04/27 20:30:00 Speed 124.9 Mbps 27 | 2023/04/27 20:30:01 Speed 93.1 Mbps 28 | 2023/04/27 20:30:01 Speed 68.2 Mbps 29 | 2023/04/27 20:30:02 Speed 47.6 Mbps 30 | 2023/04/27 20:30:02 Speed 40.6 Mbps 31 | 2023/04/27 20:30:03 Speed 44.8 Mbps 32 | 2023/04/27 20:30:03 Downloaded 125.0 MB 33 | 2023/04/27 20:30:03 Average speed 124.6 Mbps 34 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ajinasokan/fast 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math" 9 | "net/http" 10 | "regexp" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var count = 0 16 | var lastCount = 0 17 | var mu sync.Mutex 18 | 19 | func main() { 20 | log.Println("Preparing") 21 | 22 | // GET THE TOKEN 23 | 24 | resOfHtml, err := http.Get("https://fast.com/index.html") 25 | if err != nil { 26 | log.Fatal("Unable to access fast.com", err) 27 | } 28 | 29 | html, err := io.ReadAll(resOfHtml.Body) 30 | if err != nil { 31 | log.Fatal("Unable to read from fast.com", err) 32 | } 33 | 34 | rForScriptFileId := regexp.MustCompile(`src\=\"\/app-(.*?)\.js\"`) 35 | scriptFileIdMatch := rForScriptFileId.FindStringSubmatch(string(html)) 36 | if (len(scriptFileIdMatch) == 0) { 37 | log.Fatal("Unable to find js file. May be renamed from app-*.js pattern.") 38 | } 39 | scriptFileId := scriptFileIdMatch[1] 40 | 41 | res, err := http.Get("https://fast.com/app-" + scriptFileId + ".js") 42 | if err != nil { 43 | log.Fatal("Unable to access fast.com", err) 44 | } 45 | 46 | h, err := io.ReadAll(res.Body) 47 | if err != nil { 48 | log.Fatal("Unable to read from fast.com", err) 49 | } 50 | 51 | r := regexp.MustCompile(`token\:\"(.*?)\"`) 52 | tokenMatch := r.FindStringSubmatch(string(h)) 53 | if (len(tokenMatch) == 0) { 54 | log.Fatal("Unable to find token in js file. May be renamed.") 55 | } 56 | token := tokenMatch[1] 57 | 58 | // GET THE URLS 59 | 60 | res, err = http.Get("https://api.fast.com/netflix/speedtest?https=true&token=" + token) 61 | if err != nil { 62 | log.Fatal("Unable to access api.fast.com", err) 63 | } 64 | 65 | j, err := io.ReadAll(res.Body) 66 | if err != nil { 67 | log.Fatal("Unable to read from api.fast.com", err) 68 | } 69 | 70 | var urls []map[string]string 71 | json.Unmarshal(j, &urls) 72 | 73 | // MONITOR SPEED 74 | 75 | ticker := time.NewTicker(500 * time.Millisecond) 76 | done := make(chan bool) 77 | go func() { 78 | for { 79 | select { 80 | case <-done: 81 | return 82 | case <-ticker.C: 83 | mu.Lock() 84 | diff := count - lastCount 85 | lastCount = count 86 | mu.Unlock() 87 | 88 | log.Println("Speed", prettyByteSize(diff*2*8)+"bps") 89 | } 90 | } 91 | }() 92 | 93 | // FETCH URLS PARALLELY 94 | 95 | start := time.Now() 96 | var wg sync.WaitGroup 97 | for _, v := range urls { 98 | wg.Add(1) 99 | 100 | url := v["url"] 101 | 102 | go func() { 103 | defer wg.Done() 104 | download(url) 105 | }() 106 | } 107 | wg.Wait() 108 | end := time.Since(start) 109 | 110 | // SUMMARY & CLEANUP 111 | 112 | log.Println("Downloaded", prettyByteSize(count)+"B") 113 | log.Println("Average speed", prettyByteSize(int(float64(count*8)/end.Seconds()))+"bps") 114 | ticker.Stop() 115 | done <- true 116 | } 117 | 118 | func download(url string) { 119 | log.Println("Downloading", url) 120 | 121 | res, err := http.Get(url) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | buf := make([]byte, 1024*1024*10) 127 | for { 128 | n, err := res.Body.Read(buf) 129 | mu.Lock() 130 | count = count + n 131 | mu.Unlock() 132 | 133 | if err != nil { 134 | break 135 | } 136 | } 137 | } 138 | 139 | // ref: https://gist.github.com/anikitenko/b41206a49727b83a530142c76b1cb82d?permalink_comment_id=4467913#gistcomment-4467913 140 | func prettyByteSize(b int) string { 141 | bf := float64(b) 142 | for _, unit := range []string{"", "K", "M", "G", "T", "P", "E", "Z"} { 143 | if math.Abs(bf) < 1024.0 { 144 | return fmt.Sprintf("%3.1f %s", bf, unit) 145 | } 146 | bf /= 1024.0 147 | } 148 | return fmt.Sprintf("%.1f Y", bf) 149 | } 150 | --------------------------------------------------------------------------------