diff --git a/cmd/main.go b/cmd/main.go index 658d244..ba476f3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,11 +3,13 @@ package main import ( "flag" "fmt" - "net/http" + "log" "os" - "github.com/ap-pauloafonso/ratio-spoof/emulation" - "github.com/ap-pauloafonso/ratio-spoof/ratiospoof" + "github.com/ap-pauloafonso/ratio-spoof/internal/emulation" + "github.com/ap-pauloafonso/ratio-spoof/internal/input" + "github.com/ap-pauloafonso/ratio-spoof/internal/printer" + "github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof" ) func main() { @@ -50,8 +52,8 @@ required arguments: } qbit := emulation.NewQbitTorrent() - r, err := ratiospoof.NewRatioSPoofState( - ratiospoof.InputArgs{ + r, err := ratiospoof.NewRatioSpoofState( + input.InputArgs{ TorrentPath: *torrentPath, InitialDownloaded: *initialDownload, DownloadSpeed: *downloadSpeed, @@ -60,12 +62,13 @@ required arguments: Port: *port, Debug: *debug, }, - qbit, - http.DefaultClient) + qbit) if err != nil { - panic(err) + log.Fatalln(err) } + + go printer.PrintState(r) r.Run() } diff --git a/bencode/bencode.go b/internal/bencode/bencode.go similarity index 93% rename from bencode/bencode.go rename to internal/bencode/bencode.go index 3885fc3..b6478d9 100644 --- a/bencode/bencode.go +++ b/internal/bencode/bencode.go @@ -45,7 +45,13 @@ type torrentDict struct { } //TorrentDictParse decodes the bencoded bytes and builds the torrentInfo file -func TorrentDictParse(dat []byte) (*TorrentInfo, error) { +func TorrentDictParse(dat []byte) (torrent *TorrentInfo, err error) { + defer func() { + if e := recover(); e != nil { + err = e.(error) + } + }() + dict, _ := mapParse(0, &dat) torrentMap := torrentDict{resultMap: dict} return &TorrentInfo{ @@ -54,7 +60,7 @@ func TorrentDictParse(dat []byte) (*TorrentInfo, error) { TotalSize: torrentMap.extractTotalSize(), TrackerInfo: torrentMap.extractTrackerInfo(), InfoHashURLEncoded: torrentMap.extractInfoHashURLEncoded(dat), - }, nil + }, err } func (T *torrentDict) extractInfoHashURLEncoded(rawData []byte) string { @@ -115,10 +121,16 @@ func (T *torrentDict) extractTrackerInfo() *TrackerInfo { return &trackerInfo } -//Decode accepts a byte slice and returns a map with information parsed.(panic if it fails) -func Decode(data []byte) map[string]interface{} { +//Decode accepts a byte slice and returns a map with information parsed. +func Decode(data []byte) (dataMap map[string]interface{}, err error) { + defer func() { + if e := recover(); e != nil { + err = e.(error) + } + }() + result, _ := findParse(0, &data) - return result.(map[string]interface{}) + return result.(map[string]interface{}), err } func findParse(currentIdx int, data *[]byte) (result interface{}, nextIdx int) { diff --git a/bencode/bencode_test.go b/internal/bencode/bencode_test.go similarity index 97% rename from bencode/bencode_test.go rename to internal/bencode/bencode_test.go index 5cd24a2..6fd4fd1 100644 --- a/bencode/bencode_test.go +++ b/internal/bencode/bencode_test.go @@ -106,7 +106,8 @@ func TestDecode(T *testing.T) { for _, f := range files { T.Run(f.Name(), func(t *testing.T) { data, _ := ioutil.ReadFile("./torrent_files_test/" + f.Name()) - t.Log(Decode(data)["info"].(map[string]interface{})["name"]) + result, _ := Decode(data) + t.Log(result["info"].(map[string]interface{})["name"]) }) } diff --git a/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent b/internal/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent similarity index 100% rename from bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent rename to internal/bencode/torrent_files_test/Fedora-Workstation-Live-x86_64-33.torrent diff --git a/bencode/torrent_files_test/Slackware 14.2 x86_64 DVD ISO (Includes everything except for source code -- see the Slackware 14.2 source code DVD ISO above for source code).torrent b/internal/bencode/torrent_files_test/Slackware142.torrent similarity index 100% rename from bencode/torrent_files_test/Slackware 14.2 x86_64 DVD ISO (Includes everything except for source code -- see the Slackware 14.2 source code DVD ISO above for source code).torrent rename to internal/bencode/torrent_files_test/Slackware142.torrent diff --git a/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent b/internal/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent similarity index 100% rename from bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent rename to internal/bencode/torrent_files_test/ubuntu-20.04.1-desktop-amd64.iso.torrent diff --git a/emulation/qbittorrent.go b/internal/emulation/qbittorrent.go similarity index 100% rename from emulation/qbittorrent.go rename to internal/emulation/qbittorrent.go diff --git a/emulation/qbittorrent_test.go b/internal/emulation/qbittorrent_test.go similarity index 100% rename from emulation/qbittorrent_test.go rename to internal/emulation/qbittorrent_test.go diff --git a/internal/input/input.go b/internal/input/input.go new file mode 100644 index 0000000..f565a68 --- /dev/null +++ b/internal/input/input.go @@ -0,0 +1,188 @@ +package input + +import ( + "errors" + "fmt" + "math" + "strconv" + "strings" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" +) + +const ( + minPortNumber = 1 + maxPortNumber = 65535 + speedSuffixLength = 4 +) + +type InputArgs struct { + TorrentPath string + InitialDownloaded string + DownloadSpeed string + InitialUploaded string + UploadSpeed string + Port int + Debug bool +} + +type InputParsed struct { + TorrentPath string + InitialDownloaded int + DownloadSpeed int + InitialUploaded int + UploadSpeed int + Port int + Debug bool +} + +var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} +var validSpeedSufixes = [...]string{"kbps", "mbps"} + +func (I *InputArgs) ParseInput(torrentInfo *bencode.TorrentInfo) (*InputParsed, error) { + downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.TotalSize, true) + if err != nil { + return nil, err + } + uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.TotalSize, false) + if err != nil { + return nil, err + } + downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed) + if err != nil { + return nil, err + } + uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed) + if err != nil { + return nil, err + } + + if I.Port < minPortNumber || I.Port > maxPortNumber { + return nil, errors.New(fmt.Sprint("port number must be between %i and %i", minPortNumber, maxPortNumber)) + } + + return &InputParsed{InitialDownloaded: downloaded, + DownloadSpeed: downloadSpeed, + InitialUploaded: uploaded, + UploadSpeed: uploadSpeed, + Debug: I.Debug, + Port: I.Port, + }, nil +} + +func checkSpeedSufix(input string) (valid bool, suffix string) { + for _, v := range validSpeedSufixes { + + if strings.HasSuffix(strings.ToLower(input), v) { + return true, input[len(input)-4:] + } + } + return false, "" +} + +func extractInputInitialByteCount(initialSizeInput string, totalBytes int, errorIfHigher bool) (int, error) { + byteCount, err := strSize2ByteSize(initialSizeInput, totalBytes) + if err != nil { + return 0, err + } + if errorIfHigher && byteCount > totalBytes { + return 0, errors.New("initial downloaded can not be higher than the torrent size") + } + if byteCount < 0 { + return 0, errors.New("initial value can not be negative") + } + return byteCount, nil +} + +//Takes an dirty speed input and returns the bytes per second based on the suffixes +// example 1kbps(string) > 1024 bytes per second (int) +func extractInputByteSpeed(initialSpeedInput string) (int, error) { + ok, suffix := checkSpeedSufix(initialSpeedInput) + if !ok { + return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes) + } + speedVal, err := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-speedSuffixLength], 64) + if err != nil { + return 0, errors.New("invalid speed number") + } + if speedVal < 0 { + return 0, errors.New("speed can not be negative") + } + + if suffix == "kbps" { + speedVal *= 1024 + } else { + speedVal = speedVal * 1024 * 1024 + } + ret := int(speedVal) + return ret, nil +} + +func extractByteSizeNumber(strWithSufix string, sufixLength, power int) (int, error) { + v, err := strconv.ParseFloat(strWithSufix[:len(strWithSufix)-sufixLength], 64) + if err != nil { + return 0, err + } + result := v * math.Pow(1024, float64(power)) + return int(result), nil +} + +func strSize2ByteSize(input string, totalSize int) (int, error) { + lowerInput := strings.ToLower(input) + invalidSizeError := errors.New("invalid input size") + switch { + case strings.HasSuffix(lowerInput, "kb"): + { + v, err := extractByteSizeNumber(lowerInput, 2, 1) + if err != nil { + return 0, invalidSizeError + } + return v, nil + } + case strings.HasSuffix(lowerInput, "mb"): + { + v, err := extractByteSizeNumber(lowerInput, 2, 2) + if err != nil { + return 0, invalidSizeError + } + return v, nil + } + case strings.HasSuffix(lowerInput, "gb"): + { + v, err := extractByteSizeNumber(lowerInput, 2, 3) + if err != nil { + return 0, invalidSizeError + } + return v, nil + } + case strings.HasSuffix(lowerInput, "tb"): + { + v, err := extractByteSizeNumber(lowerInput, 2, 4) + if err != nil { + return 0, invalidSizeError + } + return v, nil + } + case strings.HasSuffix(lowerInput, "b"): + { + v, err := extractByteSizeNumber(lowerInput, 1, 0) + if err != nil { + return 0, invalidSizeError + } + return v, nil + } + case strings.HasSuffix(lowerInput, "%"): + { + v, err := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64) + if v < 0 || v > 100 || err != nil { + return 0, errors.New("percent value must be in (0-100)") + } + result := int(float64(v/100) * float64(totalSize)) + + return result, nil + } + + default: + return 0, errors.New("Size not found") + } +} diff --git a/internal/input/input_test.go b/internal/input/input_test.go new file mode 100644 index 0000000..86a64f7 --- /dev/null +++ b/internal/input/input_test.go @@ -0,0 +1,245 @@ +package input + +import ( + "errors" + "testing" +) + +func CheckError(out error, want error, t *testing.T) { + t.Helper() + if out == nil && want == nil { + return + } + if out != nil && want == nil { + t.Errorf("got %v, want %v", out.Error(), "") + } + if out == nil && want != nil { + t.Errorf("got %v, want %v", "", want.Error()) + } + if out != nil && want != nil && out.Error() != want.Error() { + t.Errorf("got %v, want %v", out.Error(), want.Error()) + } + +} +func TestExtractInputInitialByteCount(T *testing.T) { + data := []struct { + name string + inSize string + inTotal int + inErrorIfHigher bool + err error + }{ + { + name: "[Donwloaded - error if higher]100kb input with 200kb limit shouldn't return error test", + inSize: "100kb", + inTotal: 204800, + inErrorIfHigher: true, + }, + { + name: "[Donwloaded - error if higher]300kb input with 200kb limit should return error test", + inSize: "300kb", + inTotal: 204800, + inErrorIfHigher: true, + err: errors.New("initial downloaded can not be higher than the torrent size"), + }, + { + name: "[Uploaded]100kb input with 200kb limit shouldn't return error test", + inSize: "100kb", + inTotal: 204800, + inErrorIfHigher: false, + }, + { + name: "[Uploaded]300kb input with 200kb limit shouldn't return error test", + inSize: "300kb", + inTotal: 204800, + inErrorIfHigher: false, + }, + { + name: "[Donwloaded] -100kb should return negative number error test", + inSize: "-100kb", + inTotal: 204800, + inErrorIfHigher: true, + err: errors.New("initial value can not be negative"), + }, + { + name: "[Uploaded] -100kb should return negative number error test", + inSize: "-100kb", + inTotal: 204800, + inErrorIfHigher: false, + err: errors.New("initial value can not be negative"), + }, + } + + for _, td := range data { + T.Run(td.name, func(t *testing.T) { + _, err := extractInputInitialByteCount(td.inSize, td.inTotal, td.inErrorIfHigher) + CheckError(err, td.err, t) + }) + } +} +func TestStrSize2ByteSize(T *testing.T) { + + data := []struct { + name string + in string + inTotalSize int + out int + err error + }{ + { + name: "100kb test", + in: "100kb", + inTotalSize: 100, + out: 102400, + }, + { + name: "1kb test", + in: "1kb", + inTotalSize: 0, + out: 1024, + }, + { + name: "1mb test", + in: "1mb", + inTotalSize: 0, + out: 1048576, + }, + { + name: "1gb test", + in: "1gb", + inTotalSize: 0, + out: 1073741824, + }, + { + name: "1.5gb test", + in: "1.5gb", + inTotalSize: 0, + out: 1610612736, + }, + { + name: "1tb test", + in: "1tb", + inTotalSize: 0, + out: 1099511627776, + }, + { + name: "1b test", + in: "1b", + inTotalSize: 0, + out: 1, + }, + { + name: "10xb test", + in: "10xb", + inTotalSize: 0, + err: errors.New("invalid input size"), + }, + { + name: `100% test`, + in: "100%", + inTotalSize: 10737418240, + out: 10737418240, + }, + { + name: `55% test`, + in: "55%", + inTotalSize: 943718400, + out: 519045120, + }, + { + name: `5kg test`, + in: "5kg", + err: errors.New("Size not found"), + }, + { + name: `-1% test`, + in: "-1%", + err: errors.New("percent value must be in (0-100)"), + }, + { + name: `101% test`, + in: "101%", + err: errors.New("percent value must be in (0-100)"), + }, + { + name: `a% test`, + in: "a%", + err: errors.New("percent value must be in (0-100)"), + }, + } + + for _, td := range data { + T.Run(td.name, func(t *testing.T) { + got, err := strSize2ByteSize(td.in, td.inTotalSize) + if td.err != nil { + if td.err.Error() != err.Error() { + t.Errorf("got %v, want %v", err.Error(), td.err.Error()) + } + } + if got != td.out { + t.Errorf("got %v, want %v", got, td.out) + } + }) + } +} + +func TestExtractInputByteSpeed(T *testing.T) { + + data := []struct { + name string + speed string + expected int + err error + }{ + { + name: "1kbps test", + speed: "1kbps", + expected: 1024, + }, + { + name: "1024kbps test", + speed: "1024kbps", + expected: 1048576, + }, + { + name: "1mbps test", + speed: "1mbps", + expected: 1048576, + }, + { + name: "2.5mbps test", + speed: "2.5mbps", + expected: 2621440, + }, + { + name: "2.5tbps test", + speed: "2.5tbps", + err: errors.New("speed must be in [kbps mbps]"), + }, + { + name: "-akbps test", + speed: "-akbps", + err: errors.New("invalid speed number"), + }, + { + name: "-10kbps test", + speed: "-10kbps", + err: errors.New("speed can not be negative"), + }, + } + + for _, td := range data { + T.Run(td.name, func(t *testing.T) { + got, err := extractInputByteSpeed(td.speed) + if td.err != nil { + if td.err.Error() != err.Error() { + t.Errorf("got %v, want %v", err.Error(), td.err.Error()) + } + } + + if got != td.expected { + t.Errorf("got %v, want %v", got, td.expected) + } + }) + } +} diff --git a/ratiospoof/printstate.go b/internal/printer/printer.go similarity index 50% rename from ratiospoof/printstate.go rename to internal/printer/printer.go index ca71723..86c2f27 100644 --- a/ratiospoof/printstate.go +++ b/internal/printer/printer.go @@ -1,4 +1,4 @@ -package ratiospoof +package printer import ( "fmt" @@ -8,13 +8,14 @@ import ( "strings" "time" + "github.com/ap-pauloafonso/ratio-spoof/internal/ratiospoof" "github.com/olekukonko/ts" ) -func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { +func PrintState(state *ratiospoof.RatioSpoof) { exit := false go func() { - _ = <-exitedCH + _ = <-state.StopPrintCH exit = true }() @@ -24,19 +25,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { } width := terminalSize() clear() - if R.announceHistory.Len() > 0 { - seedersStr := fmt.Sprint(R.seeders) - leechersStr := fmt.Sprint(R.leechers) - if R.seeders == 0 { + + if state.AnnounceCount == 1 { + println("Trying to connect to the tracker...") + time.Sleep(1 * time.Second) + continue + } + if state.AnnounceHistory.Len() > 0 { + seedersStr := fmt.Sprint(state.Seeders) + leechersStr := fmt.Sprint(state.Leechers) + if state.Seeders == 0 { seedersStr = "not informed" } - if R.leechers == 0 { + if state.Leechers == 0 { leechersStr = "not informed" } var retryStr string - if R.retryAttempt > 0 { - retryStr = fmt.Sprintf("(*Retry %v - check your connection)", R.retryAttempt) + if state.Tracker.RetryAttempt > 0 { + retryStr = fmt.Sprintf("(*Retry %v - check your connection)", state.Tracker.RetryAttempt) } fmt.Printf("%s\n", center(" RATIO-SPOOF ", width-len(" RATIO-SPOOF "), "#")) fmt.Printf(` @@ -47,25 +54,25 @@ func (R *ratioSpoofState) PrintState(exitedCH <-chan string) { Download Speed: %v/s Upload Speed: %v/s Size: %v - Emulation: %v | Port: %v`, R.torrentInfo.Name, R.torrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(R.input.downloadSpeed)), - humanReadableSize(float64(R.input.uploadSpeed)), humanReadableSize(float64(R.torrentInfo.TotalSize)), R.bitTorrentClient.Name(), R.input.port) + Emulation: %v | Port: %v`, state.TorrentInfo.Name, state.TorrentInfo.TrackerInfo.Main, seedersStr, leechersStr, humanReadableSize(float64(state.Input.DownloadSpeed)), + humanReadableSize(float64(state.Input.UploadSpeed)), humanReadableSize(float64(state.TorrentInfo.TotalSize)), state.BitTorrentClient.Name(), state.Input.Port) fmt.Printf("\n\n%s\n\n", center(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF ", width-len(" GITHUB.COM/AP-PAULOAFONSO/RATIO-SPOOF "), "#")) - for i := 0; i <= R.announceHistory.Len()-2; i++ { - dequeItem := R.announceHistory.At(i).(announceEntry) - fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced\n", dequeItem.count, humanReadableSize(float64(dequeItem.downloaded)), dequeItem.percentDownloaded, humanReadableSize(float64(dequeItem.left)), humanReadableSize(float64(dequeItem.uploaded))) + for i := 0; i <= state.AnnounceHistory.Len()-2; i++ { + dequeItem := state.AnnounceHistory.At(i).(ratiospoof.AnnounceEntry) + fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | announced\n", dequeItem.Count, humanReadableSize(float64(dequeItem.Downloaded)), dequeItem.PercentDownloaded, humanReadableSize(float64(dequeItem.Left)), humanReadableSize(float64(dequeItem.Uploaded))) } - lastDequeItem := R.announceHistory.At(R.announceHistory.Len() - 1).(announceEntry) - fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.count, - humanReadableSize(float64(lastDequeItem.downloaded)), - lastDequeItem.percentDownloaded, - humanReadableSize(float64(lastDequeItem.left)), - humanReadableSize(float64(lastDequeItem.uploaded)), - fmtDuration(R.currentAnnounceTimer), + lastDequeItem := state.AnnounceHistory.At(state.AnnounceHistory.Len() - 1).(ratiospoof.AnnounceEntry) + fmt.Printf("#%v downloaded: %v(%.2f%%) | left: %v | uploaded: %v | next announce in: %v %v\n", lastDequeItem.Count, + humanReadableSize(float64(lastDequeItem.Downloaded)), + lastDequeItem.PercentDownloaded, + humanReadableSize(float64(lastDequeItem.Left)), + humanReadableSize(float64(lastDequeItem.Uploaded)), + fmtDuration(state.CurrentAnnounceTimer), retryStr) - if R.input.debug { + if state.Input.Debug { fmt.Printf("\n%s\n", center(" DEBUG ", width-len(" DEBUG "), "#")) - fmt.Printf("\n%s\n\n%s", R.lastAnounceRequest, R.lastTackerResponse) + fmt.Printf("\n%s\n\n%s", state.Tracker.LastAnounceRequest, state.Tracker.LastTackerResponse) } time.Sleep(1 * time.Second) } diff --git a/ratiospoof/printstate_test.go b/internal/printer/printer_test.go similarity index 96% rename from ratiospoof/printstate_test.go rename to internal/printer/printer_test.go index f351802..ca52f62 100644 --- a/ratiospoof/printstate_test.go +++ b/internal/printer/printer_test.go @@ -1,4 +1,4 @@ -package ratiospoof +package printer import ( "fmt" diff --git a/internal/ratiospoof/ratiospoof.go b/internal/ratiospoof/ratiospoof.go new file mode 100644 index 0000000..2c51e33 --- /dev/null +++ b/internal/ratiospoof/ratiospoof.go @@ -0,0 +1,237 @@ +package ratiospoof + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "math/rand" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" + "github.com/ap-pauloafonso/ratio-spoof/internal/input" + "github.com/ap-pauloafonso/ratio-spoof/internal/tracker" + "github.com/gammazero/deque" +) + +const ( + maxAnnounceHistory = 10 +) + +type RatioSpoof struct { + mutex *sync.Mutex + TorrentInfo *bencode.TorrentInfo + Input *input.InputParsed + Tracker *tracker.HttpTracker + BitTorrentClient TorrentClientEmulation + CurrentAnnounceTimer int + AnnounceInterval int + NumWant int + Seeders int + Leechers int + AnnounceCount int + Status string + AnnounceHistory announceHistory + timerUpdateCh chan int + StopPrintCH chan interface{} +} + +type TorrentClientEmulation interface { + PeerID() string + Key() string + Query() string + Name() string + Headers() map[string]string + NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) +} + +type AnnounceEntry struct { + Count int + Downloaded int + PercentDownloaded float32 + Uploaded int + Left int +} + +type announceHistory struct { + deque.Deque +} + +func NewRatioSpoofState(input input.InputArgs, torrentClient TorrentClientEmulation) (*RatioSpoof, error) { + changeTimerCh := make(chan int) + stopPrintCh := make(chan interface{}) + dat, err := ioutil.ReadFile(input.TorrentPath) + if err != nil { + return nil, err + } + + torrentInfo, err := bencode.TorrentDictParse(dat) + if err != nil { + return nil, errors.New("failed to parse the torrent file") + } + + httpTracker, err := tracker.NewHttpTracker(torrentInfo, changeTimerCh) + if err != nil { + return nil, err + } + + inputParsed, err := input.ParseInput(torrentInfo) + if err != nil { + return nil, err + } + + return &RatioSpoof{ + BitTorrentClient: torrentClient, + TorrentInfo: torrentInfo, + Tracker: httpTracker, + Input: inputParsed, + NumWant: 200, + Status: "started", + mutex: &sync.Mutex{}, + timerUpdateCh: changeTimerCh, + StopPrintCH: stopPrintCh, + }, nil +} + +func (A *announceHistory) pushValueHistory(value AnnounceEntry) { + if A.Len() >= maxAnnounceHistory { + A.PopFront() + } + A.PushBack(value) +} + +func (R *RatioSpoof) gracefullyExit() { + fmt.Printf("\nGracefully exiting...\n") + R.Status = "stopped" + R.NumWant = 0 + R.fireAnnounce(false) + fmt.Printf("Gracefully exited successfully.\n") + +} + +func (R *RatioSpoof) Run() { + rand.Seed(time.Now().UnixNano()) + sigCh := make(chan os.Signal) + + signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + R.firstAnnounce() + go R.decreaseTimer() + go R.updateTimer() + go func() { + for { + R.generateNextAnnounce() + time.Sleep(time.Duration(R.AnnounceInterval) * time.Second) + R.fireAnnounce(true) + } + }() + <-sigCh + R.StopPrintCH <- "exit print" + R.gracefullyExit() +} +func (R *RatioSpoof) firstAnnounce() { + R.addAnnounce(R.Input.InitialDownloaded, R.Input.InitialUploaded, calculateBytesLeft(R.Input.InitialDownloaded, R.TorrentInfo.TotalSize), (float32(R.Input.InitialDownloaded)/float32(R.TorrentInfo.TotalSize))*100) + R.fireAnnounce(false) +} + +func (R *RatioSpoof) updateInterval(resp tracker.TrackerResponse) { + if resp.Interval > 0 { + R.AnnounceInterval = resp.Interval + } else { + R.AnnounceInterval = 1800 + } +} + +func (R *RatioSpoof) updateSeedersAndLeechers(resp tracker.TrackerResponse) { + R.Seeders = resp.Seeders + R.Leechers = resp.Leechers +} +func (R *RatioSpoof) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) { + R.AnnounceCount++ + R.AnnounceHistory.pushValueHistory(AnnounceEntry{Count: R.AnnounceCount, Downloaded: currentDownloaded, Uploaded: currentUploaded, Left: currentLeft, PercentDownloaded: percentDownloaded}) +} +func (R *RatioSpoof) fireAnnounce(retry bool) error { + lastAnnounce := R.AnnounceHistory.Back().(AnnounceEntry) + replacer := strings.NewReplacer("{infohash}", R.TorrentInfo.InfoHashURLEncoded, + "{port}", fmt.Sprint(R.Input.Port), + "{peerid}", R.BitTorrentClient.PeerID(), + "{uploaded}", fmt.Sprint(lastAnnounce.Uploaded), + "{downloaded}", fmt.Sprint(lastAnnounce.Downloaded), + "{left}", fmt.Sprint(lastAnnounce.Left), + "{key}", R.BitTorrentClient.Key(), + "{event}", R.Status, + "{numwant}", fmt.Sprint(R.NumWant)) + query := replacer.Replace(R.BitTorrentClient.Query()) + trackerResp, err := R.Tracker.Announce(query, R.BitTorrentClient.Headers(), retry, R.timerUpdateCh) + if err != nil { + log.Fatalf("failed to reach the tracker:\n%s ", err.Error()) + } + + if trackerResp != nil { + R.updateSeedersAndLeechers(*trackerResp) + R.updateInterval(*trackerResp) + } + return nil +} +func (R *RatioSpoof) generateNextAnnounce() { + R.timerUpdateCh <- R.AnnounceInterval + lastAnnounce := R.AnnounceHistory.Back().(AnnounceEntry) + currentDownloaded := lastAnnounce.Downloaded + var downloadCandidate int + + if currentDownloaded < R.TorrentInfo.TotalSize { + downloadCandidate = calculateNextTotalSizeByte(R.Input.DownloadSpeed, currentDownloaded, R.TorrentInfo.PieceSize, R.AnnounceInterval, R.TorrentInfo.TotalSize) + } else { + downloadCandidate = R.TorrentInfo.TotalSize + } + + currentUploaded := lastAnnounce.Uploaded + uploadCandidate := calculateNextTotalSizeByte(R.Input.UploadSpeed, currentUploaded, R.TorrentInfo.PieceSize, R.AnnounceInterval, 0) + + leftCandidate := calculateBytesLeft(downloadCandidate, R.TorrentInfo.TotalSize) + + d, u, l := R.BitTorrentClient.NextAmountReport(downloadCandidate, uploadCandidate, leftCandidate, R.TorrentInfo.PieceSize) + + R.addAnnounce(d, u, l, (float32(d)/float32(R.TorrentInfo.TotalSize))*100) +} +func (R *RatioSpoof) decreaseTimer() { + for { + time.Sleep(1 * time.Second) + R.mutex.Lock() + if R.CurrentAnnounceTimer > 0 { + R.CurrentAnnounceTimer-- + } + R.mutex.Unlock() + } +} + +func (R *RatioSpoof) updateTimer() { + for { + newValue := <-R.timerUpdateCh + R.mutex.Lock() + R.CurrentAnnounceTimer = newValue + R.mutex.Unlock() + } +} + +func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int { + if speedBytePerSecond == 0 { + return currentByte + } + totalCandidate := currentByte + (speedBytePerSecond * seconds) + randomPieces := rand.Intn(10-1) + 1 + totalCandidate = totalCandidate + (pieceSizeByte * randomPieces) + + if limitTotalBytes != 0 && totalCandidate > limitTotalBytes { + return limitTotalBytes + } + return totalCandidate +} + +func calculateBytesLeft(currentBytes, totalBytes int) int { + return totalBytes - currentBytes +} diff --git a/ratiospoof/ratiospoof_test.go b/internal/ratiospoof/ratiospoof_test.go similarity index 59% rename from ratiospoof/ratiospoof_test.go rename to internal/ratiospoof/ratiospoof_test.go index ab8fc8d..b742e3d 100644 --- a/ratiospoof/ratiospoof_test.go +++ b/internal/ratiospoof/ratiospoof_test.go @@ -1,7 +1,6 @@ package ratiospoof import ( - "fmt" "testing" ) @@ -12,34 +11,6 @@ func assertAreEqual(t *testing.T, got, want interface{}) { } } -func TestStrSize2ByteSize(T *testing.T) { - - data := []struct { - in string - inTotalSize int - out int - }{ - {"100kb", 100, 102400}, - {"1kb", 0, 1024}, - {"1mb", 0, 1048576}, - {"1gb", 0, 1073741824}, - {"1.5gb", 0, 1610612736}, - {"1tb", 0, 1099511627776}, - {"1b", 0, 1}, - {"100%", 10737418240, 10737418240}, - {"55%", 943718400, 519045120}, - } - - for idx, td := range data { - T.Run(fmt.Sprint(idx), func(t *testing.T) { - got := strSize2ByteSize(td.in, td.inTotalSize) - if got != td.out { - t.Errorf("got %v, want %v", got, td.out) - } - }) - } -} - func TestClculateNextTotalSizeByte(T *testing.T) { got := calculateNextTotalSizeByte(100*1024, 0, 512, 30, 87979879) diff --git a/internal/tracker/tracker.go b/internal/tracker/tracker.go new file mode 100644 index 0000000..ebb2c0d --- /dev/null +++ b/internal/tracker/tracker.go @@ -0,0 +1,140 @@ +package tracker + +import ( + "bytes" + "compress/gzip" + "errors" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/ap-pauloafonso/ratio-spoof/internal/bencode" +) + +type HttpTracker struct { + Urls []string + RetryAttempt int + LastAnounceRequest string + LastTackerResponse string +} + +type TrackerResponse struct { + MinInterval int + Interval int + Seeders int + Leechers int +} + +func NewHttpTracker(torrentInfo *bencode.TorrentInfo, timerChangeChannel chan<- int) (*HttpTracker, error) { + + var result []string + for _, url := range torrentInfo.TrackerInfo.Urls { + if strings.HasPrefix(url, "http") { + result = append(result, url) + } + } + if len(result) == 0 { + return nil, errors.New("No tcp/http tracker url announce found") + } + return &HttpTracker{Urls: torrentInfo.TrackerInfo.Urls}, nil +} + +func (T *HttpTracker) SwapFirst(currentIdx int) { + aux := T.Urls[0] + T.Urls[0] = T.Urls[currentIdx] + T.Urls[currentIdx] = aux +} + +func (T *HttpTracker) Announce(query string, headers map[string]string, retry bool, timerUpdateChannel chan<- int) (*TrackerResponse, error) { + defer func() { + T.RetryAttempt = 0 + }() + if retry { + retryDelay := 30 * time.Second + for { + trackerResp, err := T.tryMakeRequest(query, headers) + if err != nil { + timerUpdateChannel <- int(retryDelay.Seconds()) + T.RetryAttempt++ + time.Sleep(retryDelay) + retryDelay *= 2 + if retryDelay.Seconds() > 900 { + retryDelay = 900 + } + continue + } + return trackerResp, nil + } + + } else { + resp, err := T.tryMakeRequest(query, headers) + if err != nil { + return nil, err + } + return resp, nil + } +} + +func (t *HttpTracker) tryMakeRequest(query string, headers map[string]string) (*TrackerResponse, error) { + for idx, baseUrl := range t.Urls { + completeURL := buildFullUrl(baseUrl, query) + t.LastAnounceRequest = completeURL + req, _ := http.NewRequest("GET", completeURL, nil) + for header, value := range headers { + req.Header.Add(header, value) + } + resp, err := http.DefaultClient.Do(req) + if err == nil { + if resp.StatusCode == http.StatusOK { + bytesR, _ := ioutil.ReadAll(resp.Body) + if len(bytesR) == 0 { + continue + } + mimeType := http.DetectContentType(bytesR) + if mimeType == "application/x-gzip" { + gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR)) + bytesR, _ = ioutil.ReadAll(gzipReader) + gzipReader.Close() + } + t.LastTackerResponse = string(bytesR) + decodedResp, err := bencode.Decode(bytesR) + if err != nil { + continue + } + ret, err := extractTrackerResponse(decodedResp) + if err != nil { + continue + } + if idx != 0 { + t.SwapFirst(idx) + } + + return &ret, nil + } + resp.Body.Close() + } + } + return nil, errors.New("Connection error with the tracker") + +} + +func buildFullUrl(baseurl, query string) string { + if len(strings.Split(baseurl, "?")) > 1 { + return baseurl + "&" + strings.TrimLeft(query, "&") + } + return baseurl + "?" + strings.TrimLeft(query, "?") +} + +func extractTrackerResponse(datatrackerResponse map[string]interface{}) (TrackerResponse, error) { + var result TrackerResponse + if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 { + return result, errors.New(v) + } + result.MinInterval, _ = datatrackerResponse["min interval"].(int) + result.Interval, _ = datatrackerResponse["interval"].(int) + result.Seeders, _ = datatrackerResponse["complete"].(int) + result.Leechers, _ = datatrackerResponse["incomplete"].(int) + return result, nil + +} diff --git a/ratiospoof/ratiospoof.go b/ratiospoof/ratiospoof.go deleted file mode 100644 index cf2c4e7..0000000 --- a/ratiospoof/ratiospoof.go +++ /dev/null @@ -1,486 +0,0 @@ -package ratiospoof - -import ( - "bytes" - "compress/gzip" - "errors" - "fmt" - "io/ioutil" - "math" - "math/rand" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/ap-pauloafonso/ratio-spoof/bencode" - "github.com/gammazero/deque" -) - -const ( - maxAnnounceHistory = 10 -) - -var validInitialSufixes = [...]string{"%", "b", "kb", "mb", "gb", "tb"} -var validSpeedSufixes = [...]string{"kbps", "mbps"} - -type ratioSpoofState struct { - mutex *sync.Mutex - httpClient HttpClient - torrentInfo *bencode.TorrentInfo - input *inputParsed - trackerState *httpTracker - bitTorrentClient TorrentClientEmulation - currentAnnounceTimer int - announceInterval int - numWant int - seeders int - leechers int - announceCount int - status string - announceHistory announceHistory - lastAnounceRequest string - lastTackerResponse string - retryAttempt int -} -type httpTracker struct { - urls []string -} - -func newHttpTracker(torrentInfo *bencode.TorrentInfo) (*httpTracker, error) { - - var result []string - for _, url := range torrentInfo.TrackerInfo.Urls { - if strings.HasPrefix(url, "http") { - result = append(result, url) - } - } - if len(result) == 0 { - return nil, errors.New("No tcp/http tracker url announce found") - } - return &httpTracker{urls: torrentInfo.TrackerInfo.Urls}, nil -} - -func (T *httpTracker) SwapFirst(currentIdx int) { - aux := T.urls[0] - T.urls[0] = T.urls[currentIdx] - T.urls[currentIdx] = aux -} - -type InputArgs struct { - TorrentPath string - InitialDownloaded string - DownloadSpeed string - InitialUploaded string - UploadSpeed string - Port int - Debug bool -} - -type inputParsed struct { - torrentPath string - initialDownloaded int - downloadSpeed int - initialUploaded int - uploadSpeed int - port int - debug bool -} - -func (I *InputArgs) parseInput(torrentInfo *bencode.TorrentInfo) (*inputParsed, error) { - downloaded, err := extractInputInitialByteCount(I.InitialDownloaded, torrentInfo.TotalSize, true) - if err != nil { - return nil, err - } - uploaded, err := extractInputInitialByteCount(I.InitialUploaded, torrentInfo.TotalSize, false) - if err != nil { - return nil, err - } - downloadSpeed, err := extractInputByteSpeed(I.DownloadSpeed) - if err != nil { - return nil, err - } - uploadSpeed, err := extractInputByteSpeed(I.UploadSpeed) - if err != nil { - return nil, err - } - - if I.Port < 1 || I.Port > 65535 { - return nil, errors.New("port number must be between 1 and 65535") - } - - return &inputParsed{initialDownloaded: downloaded, - downloadSpeed: downloadSpeed, - initialUploaded: uploaded, - uploadSpeed: uploadSpeed, - debug: I.Debug, - port: I.Port, - }, nil -} - -func NewRatioSPoofState(input InputArgs, torrentClient TorrentClientEmulation, httpclient HttpClient) (*ratioSpoofState, error) { - - dat, err := ioutil.ReadFile(input.TorrentPath) - if err != nil { - return nil, err - } - - torrentInfo, err := bencode.TorrentDictParse(dat) - if err != nil { - panic(err) - } - - httpTracker, err := newHttpTracker(torrentInfo) - if err != nil { - panic(err) - } - - inputParsed, err := input.parseInput(torrentInfo) - if err != nil { - panic(err) - } - - return &ratioSpoofState{ - bitTorrentClient: torrentClient, - httpClient: httpclient, - torrentInfo: torrentInfo, - trackerState: httpTracker, - input: inputParsed, - numWant: 200, - status: "started", - mutex: &sync.Mutex{}, - }, nil -} - -func checkSpeedSufix(input string) (valid bool, suffix string) { - for _, v := range validSpeedSufixes { - - if strings.HasSuffix(strings.ToLower(input), v) { - return true, input[len(input)-4:] - } - } - return false, "" -} - -func extractInputInitialByteCount(initialSizeInput string, totalBytes int, errorIfHigher bool) (int, error) { - byteCount := strSize2ByteSize(initialSizeInput, totalBytes) - if errorIfHigher && byteCount > totalBytes { - return 0, errors.New("initial downloaded can not be higher than the torrent size") - } - if byteCount < 0 { - return 0, errors.New("initial value can not be negative") - } - return byteCount, nil -} -func extractInputByteSpeed(initialSpeedInput string) (int, error) { - ok, suffix := checkSpeedSufix(initialSpeedInput) - if !ok { - return 0, fmt.Errorf("speed must be in %v", validSpeedSufixes) - } - number, _ := strconv.ParseFloat(initialSpeedInput[:len(initialSpeedInput)-4], 64) - if number < 0 { - return 0, errors.New("speed can not be negative") - } - - if suffix == "kbps" { - number *= 1024 - } else { - number = number * 1024 * 1024 - } - ret := int(number) - return ret, nil -} - -type trackerResponse struct { - minInterval int - interval int - seeders int - leechers int -} - -type TorrentClientEmulation interface { - PeerID() string - Key() string - Query() string - Name() string - Headers() map[string]string - NextAmountReport(DownloadCandidateNextAmount, UploadCandidateNextAmount, leftCandidateNextAmount, pieceSize int) (downloaded, uploaded, left int) -} -type HttpClient interface { - Do(req *http.Request) (*http.Response, error) -} - -type announceEntry struct { - count int - downloaded int - percentDownloaded float32 - uploaded int - left int -} - -type announceHistory struct { - deque.Deque -} - -func (A *announceHistory) pushValueHistory(value announceEntry) { - if A.Len() >= maxAnnounceHistory { - A.PopFront() - } - A.PushBack(value) -} - -func (R *ratioSpoofState) gracefullyExit() { - fmt.Printf("\nGracefully exiting...\n") - R.status = "stopped" - R.numWant = 0 - R.fireAnnounce(false) -} - -func (R *ratioSpoofState) Run() { - rand.Seed(time.Now().UnixNano()) - sigCh := make(chan os.Signal) - stopPrintCh := make(chan string) - - signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - R.firstAnnounce() - go R.decreaseTimer() - go R.PrintState(stopPrintCh) - go func() { - for { - R.generateNextAnnounce() - time.Sleep(time.Duration(R.announceInterval) * time.Second) - R.fireAnnounce(true) - } - }() - <-sigCh - stopPrintCh <- "exit print" - R.gracefullyExit() -} -func (R *ratioSpoofState) firstAnnounce() { - println("Trying to connect to the tracker...") - R.addAnnounce(R.input.initialDownloaded, R.input.initialUploaded, calculateBytesLeft(R.input.initialDownloaded, R.torrentInfo.TotalSize), (float32(R.input.initialDownloaded)/float32(R.torrentInfo.TotalSize))*100) - R.fireAnnounce(false) -} - -func (R *ratioSpoofState) updateInterval(resp trackerResponse) { - if resp.interval > 0 { - R.announceInterval = resp.interval - } else { - R.announceInterval = 1800 - } -} - -func (R *ratioSpoofState) updateSeedersAndLeechers(resp trackerResponse) { - R.seeders = resp.seeders - R.leechers = resp.leechers -} -func (R *ratioSpoofState) addAnnounce(currentDownloaded, currentUploaded, currentLeft int, percentDownloaded float32) { - R.announceCount++ - R.announceHistory.pushValueHistory(announceEntry{count: R.announceCount, downloaded: currentDownloaded, uploaded: currentUploaded, left: currentLeft, percentDownloaded: percentDownloaded}) -} -func (R *ratioSpoofState) fireAnnounce(retry bool) { - lastAnnounce := R.announceHistory.Back().(announceEntry) - replacer := strings.NewReplacer("{infohash}", R.torrentInfo.InfoHashURLEncoded, - "{port}", fmt.Sprint(R.input.port), - "{peerid}", R.bitTorrentClient.PeerID(), - "{uploaded}", fmt.Sprint(lastAnnounce.uploaded), - "{downloaded}", fmt.Sprint(lastAnnounce.downloaded), - "{left}", fmt.Sprint(lastAnnounce.left), - "{key}", R.bitTorrentClient.Key(), - "{event}", R.status, - "{numwant}", fmt.Sprint(R.numWant)) - query := replacer.Replace(R.bitTorrentClient.Query()) - - var trackerResp *trackerResponse - if retry { - retryDelay := 30 * time.Second - for { - exit := false - func() { - defer func() { - if err := recover(); err != nil { - R.changeCurrentTimer(int(retryDelay.Seconds())) - R.retryAttempt++ - time.Sleep(retryDelay) - retryDelay *= 2 - if retryDelay.Seconds() > 900 { - retryDelay = 900 - } - } - }() - trackerResp = R.tryMakeRequest(query) - exit = true - }() - if exit { - break - } - } - - } else { - trackerResp = R.tryMakeRequest(query) - } - R.retryAttempt = 0 - if trackerResp != nil { - R.updateSeedersAndLeechers(*trackerResp) - R.updateInterval(*trackerResp) - } -} -func (R *ratioSpoofState) generateNextAnnounce() { - R.changeCurrentTimer(R.announceInterval) - lastAnnounce := R.announceHistory.Back().(announceEntry) - currentDownloaded := lastAnnounce.downloaded - var downloadCandidate int - - if currentDownloaded < R.torrentInfo.TotalSize { - downloadCandidate = calculateNextTotalSizeByte(R.input.downloadSpeed, currentDownloaded, R.torrentInfo.PieceSize, R.currentAnnounceTimer, R.torrentInfo.TotalSize) - } else { - downloadCandidate = R.torrentInfo.TotalSize - } - - currentUploaded := lastAnnounce.uploaded - uploadCandidate := calculateNextTotalSizeByte(R.input.uploadSpeed, currentUploaded, R.torrentInfo.PieceSize, R.currentAnnounceTimer, 0) - - leftCandidate := calculateBytesLeft(downloadCandidate, R.torrentInfo.TotalSize) - - d, u, l := R.bitTorrentClient.NextAmountReport(downloadCandidate, uploadCandidate, leftCandidate, R.torrentInfo.PieceSize) - - R.addAnnounce(d, u, l, (float32(d)/float32(R.torrentInfo.TotalSize))*100) -} -func (R *ratioSpoofState) decreaseTimer() { - for { - time.Sleep(1 * time.Second) - R.mutex.Lock() - if R.currentAnnounceTimer > 0 { - R.currentAnnounceTimer-- - } - R.mutex.Unlock() - } -} - -func (R *ratioSpoofState) changeCurrentTimer(newAnnounceRate int) { - R.mutex.Lock() - R.currentAnnounceTimer = newAnnounceRate - R.mutex.Unlock() -} - -func (R *ratioSpoofState) tryMakeRequest(query string) *trackerResponse { - for idx, baseUrl := range R.trackerState.urls { - completeURL := buildFullUrl(baseUrl, query) - R.lastAnounceRequest = completeURL - req, _ := http.NewRequest("GET", completeURL, nil) - for header, value := range R.bitTorrentClient.Headers() { - req.Header.Add(header, value) - } - resp, err := R.httpClient.Do(req) - if err == nil { - if resp.StatusCode == http.StatusOK { - bytesR, _ := ioutil.ReadAll(resp.Body) - if len(bytesR) == 0 { - return nil - } - mimeType := http.DetectContentType(bytesR) - if mimeType == "application/x-gzip" { - gzipReader, _ := gzip.NewReader(bytes.NewReader(bytesR)) - bytesR, _ = ioutil.ReadAll(gzipReader) - gzipReader.Close() - } - R.lastTackerResponse = string(bytesR) - decodedResp := bencode.Decode(bytesR) - if idx != 0 { - R.trackerState.SwapFirst(idx) - } - ret := extractTrackerResponse(decodedResp) - return &ret - } - resp.Body.Close() - } - } - panic("Connection error with the tracker") - -} - -func buildFullUrl(baseurl, query string) string { - if len(strings.Split(baseurl, "?")) > 1 { - return baseurl + "&" + strings.TrimLeft(query, "&") - } - return baseurl + "?" + strings.TrimLeft(query, "?") -} - -func calculateNextTotalSizeByte(speedBytePerSecond, currentByte, pieceSizeByte, seconds, limitTotalBytes int) int { - if speedBytePerSecond == 0 { - return currentByte - } - totalCandidate := currentByte + (speedBytePerSecond * seconds) - randomPieces := rand.Intn(10-1) + 1 - totalCandidate = totalCandidate + (pieceSizeByte * randomPieces) - - if limitTotalBytes != 0 && totalCandidate > limitTotalBytes { - return limitTotalBytes - } - return totalCandidate -} - -func extractTrackerResponse(datatrackerResponse map[string]interface{}) trackerResponse { - var result trackerResponse - if v, ok := datatrackerResponse["failure reason"].(string); ok && len(v) > 0 { - panic(errors.New(v)) - } - result.minInterval, _ = datatrackerResponse["min interval"].(int) - result.interval, _ = datatrackerResponse["interval"].(int) - result.seeders, _ = datatrackerResponse["complete"].(int) - result.leechers, _ = datatrackerResponse["incomplete"].(int) - return result - -} -func calculateBytesLeft(currentBytes, totalBytes int) int { - return totalBytes - currentBytes -} - -func strSize2ByteSize(input string, totalSize int) int { - lowerInput := strings.ToLower(input) - - parseStrNumberFn := func(strWithSufix string, sufixLength, n int) int { - v, _ := strconv.ParseFloat(strWithSufix[:len(lowerInput)-sufixLength], 64) - result := v * math.Pow(1024, float64(n)) - return int(result) - } - switch { - case strings.HasSuffix(lowerInput, "kb"): - { - return parseStrNumberFn(lowerInput, 2, 1) - } - case strings.HasSuffix(lowerInput, "mb"): - { - return parseStrNumberFn(lowerInput, 2, 2) - } - case strings.HasSuffix(lowerInput, "gb"): - { - return parseStrNumberFn(lowerInput, 2, 3) - } - case strings.HasSuffix(lowerInput, "tb"): - { - return parseStrNumberFn(lowerInput, 2, 4) - } - case strings.HasSuffix(lowerInput, "b"): - { - return parseStrNumberFn(lowerInput, 1, 0) - } - case strings.HasSuffix(lowerInput, "%"): - { - v, _ := strconv.ParseFloat(lowerInput[:len(lowerInput)-1], 64) - if v < 0 || v > 100 { - panic("percent value must be in (0-100)") - } - result := int(float64(v/100) * float64(totalSize)) - - return result - } - - default: - panic("Size not found") - } -}