├── README.md ├── .gitignore ├── LICENSE ├── Makefile ├── tracking.go ├── scrapmagnet.go ├── http.go └── bittorrent.go /README.md: -------------------------------------------------------------------------------- 1 | # scrapmagnet 2 | Magnet link streamer written in Go. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | android_arm 27 | darwin_amd64 28 | linux_386 29 | linux_amd64 30 | linux_arm 31 | windows_386 32 | 33 | .DS_Store 34 | deploy.sh 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Texier 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Common 3 | ############################################################################### 4 | NAME = scrapmagnet 5 | 6 | ############################################################################### 7 | # Development environment 8 | ############################################################################### 9 | PLATFORMS = android-arm \ 10 | darwin-x64 \ 11 | linux-x86 \ 12 | linux-x64 \ 13 | linux-arm \ 14 | windows-x86 \ 15 | windows-x64 16 | 17 | DOCKER = docker 18 | DOCKER_IMAGE = sharkone/libtorrent-go 19 | 20 | all: build 21 | 22 | build: 23 | for i in $(PLATFORMS); do \ 24 | $(DOCKER) run -ti --rm -v $(HOME):$(HOME) -e GOPATH=$(shell go env GOPATH) -w $(shell pwd) $(DOCKER_IMAGE):$$i make cc-build || exit 1; \ 25 | done 26 | 27 | ############################################################################### 28 | # Cross-compilation environment (inside each Docker image) 29 | ############################################################################### 30 | ifeq ($(CROSS_GOOS), windows) 31 | OUT_NAME = $(NAME).exe 32 | else 33 | OUT_NAME = $(NAME) 34 | endif 35 | 36 | cc-build: 37 | GOOS=$(CROSS_GOOS) GOARCH=$(CROSS_GOARCH) GOARM=$(CROSS_GOARM) go build -v -x -o $(CROSS_GOOS)_$(CROSS_GOARCH)/$(OUT_NAME) 38 | -------------------------------------------------------------------------------- /tracking.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "runtime" 12 | 13 | "github.com/dukex/mixpanel" 14 | ) 15 | 16 | var ( 17 | publicIP = "" 18 | ) 19 | 20 | func peopleSet() { 21 | properties := make(map[string]interface{}) 22 | properties["Server OS"] = runtime.GOOS 23 | properties["Server Arch"] = runtime.GOARCH 24 | 25 | if settings.mixpanelData != "" { 26 | if data, err := base64.StdEncoding.DecodeString(settings.mixpanelData); err == nil { 27 | json.Unmarshal([]byte(data), &properties) 28 | } else { 29 | log.Print(err) 30 | } 31 | } 32 | 33 | client := mixpanel.NewMixpanel(settings.mixpanelToken) 34 | people := client.Identify(getDistinctId()) 35 | people.Update("$set", properties) 36 | } 37 | 38 | func trackingEvent(eventName string, properties map[string]interface{}, mixpanelData string) { 39 | properties["Server OS"] = runtime.GOOS 40 | properties["Server Arch"] = runtime.GOARCH 41 | 42 | if settings.mixpanelData != "" { 43 | if data, err := base64.StdEncoding.DecodeString(settings.mixpanelData); err == nil { 44 | json.Unmarshal([]byte(data), &properties) 45 | } else { 46 | log.Print(err) 47 | } 48 | } 49 | 50 | if mixpanelData != "" { 51 | if data, err := base64.StdEncoding.DecodeString(mixpanelData); err == nil { 52 | json.Unmarshal([]byte(data), &properties) 53 | } else { 54 | log.Print(err) 55 | } 56 | } 57 | 58 | client := mixpanel.NewMixpanel(settings.mixpanelToken) 59 | client.Track(getDistinctId(), eventName, properties) 60 | } 61 | 62 | func getPublicIP() string { 63 | if publicIP == "" { 64 | if resp, err := http.Get("http://myexternalip.com/raw"); err == nil { 65 | defer resp.Body.Close() 66 | if content, err := ioutil.ReadAll(resp.Body); err == nil { 67 | publicIP = string(content) 68 | } else { 69 | log.Panic(err) 70 | } 71 | } else { 72 | log.Panic(err) 73 | } 74 | } 75 | 76 | return publicIP 77 | } 78 | 79 | func getDistinctId() string { 80 | data := []byte(runtime.GOOS + runtime.GOARCH + getPublicIP()) 81 | return fmt.Sprintf("%x", sha1.Sum(data)) 82 | } 83 | -------------------------------------------------------------------------------- /scrapmagnet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | ) 7 | 8 | type Settings struct { 9 | parentPID int 10 | httpPort int 11 | bitTorrentPort int 12 | uPNPNatPMPEnabled bool 13 | maxDownloadRate int 14 | maxUploadRate int 15 | keepFiles bool 16 | inactivityPauseTimeout int 17 | inactivityRemoveTimeout int 18 | proxyType string 19 | proxyHost string 20 | proxyPort int 21 | proxyUser string 22 | proxyPassword string 23 | mixpanelToken string 24 | mixpanelData string 25 | } 26 | 27 | var ( 28 | settings = Settings{} 29 | bitTorrent *BitTorrent 30 | httpServer *Http 31 | ) 32 | 33 | func main() { 34 | flag.IntVar(&settings.parentPID, "ppid", -1, "Parent PID to monitor and auto-shutdown") 35 | flag.IntVar(&settings.httpPort, "http-port", 8042, "Port used for HTTP server") 36 | flag.IntVar(&settings.bitTorrentPort, "bittorrent-port", 6900, "Port used for BitTorrent incoming connections") 37 | flag.BoolVar(&settings.uPNPNatPMPEnabled, "upnp-natpmp-enabled", true, "Enable UPNP/NATPMP") 38 | flag.IntVar(&settings.maxDownloadRate, "max-download-rate", 0, "Maximum download rate in kB/s, 0 = Unlimited") 39 | flag.IntVar(&settings.maxUploadRate, "max-upload-rate", 0, "Maximum upload rate in kB/s, 0 = Unlimited") 40 | flag.BoolVar(&settings.keepFiles, "keep-files", false, "Keep downloaded files upon stopping") 41 | flag.IntVar(&settings.inactivityPauseTimeout, "inactivity-pause-timeout", 4, "Torrents will be paused after some inactivity") 42 | flag.IntVar(&settings.inactivityRemoveTimeout, "inactivity-remove-timeout", 600, "Torrents will be removed after some inactivity") 43 | flag.StringVar(&settings.proxyType, "proxy-type", "None", "Proxy type: None/SOCKS5") 44 | flag.StringVar(&settings.proxyHost, "proxy-host", "", "Proxy host (ex: myproxy.com, 1.2.3.4") 45 | flag.IntVar(&settings.proxyPort, "proxy-port", 1080, "Proxy port") 46 | flag.StringVar(&settings.proxyUser, "proxy-user", "", "Proxy user") 47 | flag.StringVar(&settings.proxyPassword, "proxy-password", "", "Proxy password") 48 | flag.StringVar(&settings.mixpanelToken, "mixpanel-token", "", "Mixpanel token") 49 | flag.StringVar(&settings.mixpanelData, "mixpanel-data", "", "Mixpanel data") 50 | flag.Parse() 51 | 52 | bitTorrent = NewBitTorrent() 53 | httpServer = NewHttp(bitTorrent) 54 | 55 | log.Print("[scrapmagnet] Starting") 56 | bitTorrent.Start() 57 | httpServer.Start() 58 | httpServer.Stop() 59 | bitTorrent.Stop() 60 | log.Print("[scrapmagnet] Stopping") 61 | } 62 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "mime" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/drone/routes" 14 | "github.com/mitchellh/go-ps" 15 | "github.com/stretchr/graceful" 16 | ) 17 | 18 | var httpInstance *Http = nil 19 | 20 | type Http struct { 21 | bitTorrent *BitTorrent 22 | server *graceful.Server 23 | } 24 | 25 | func NewHttp(bitTorrent *BitTorrent) *Http { 26 | mime.AddExtensionType(".avi", "video/avi") 27 | mime.AddExtensionType(".mkv", "video/x-matroska") 28 | mime.AddExtensionType(".mp4", "video/mp4") 29 | 30 | mux := routes.New() 31 | mux.Get("/", index) 32 | mux.Get("/video", video) 33 | mux.Get("/shutdown", shutdown) 34 | 35 | return &Http{ 36 | bitTorrent: bitTorrent, 37 | server: &graceful.Server{ 38 | Timeout: 500 * time.Millisecond, 39 | Server: &http.Server{ 40 | Addr: fmt.Sprintf("%v:%v", "0.0.0.0", settings.httpPort), 41 | Handler: mux, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func (h *Http) Start() { 48 | // Parent process monitoring 49 | if settings.parentPID != -1 { 50 | go func() { 51 | for { 52 | p, err := ps.FindProcess(settings.parentPID) 53 | if p == nil || err != nil { 54 | log.Print("[scrapmagnet] Parent process is dead, exiting") 55 | httpInstance.server.Stop(500 * time.Millisecond) 56 | return 57 | } 58 | time.Sleep(time.Second) 59 | } 60 | }() 61 | } 62 | 63 | httpInstance = h 64 | err := h.server.ListenAndServe() 65 | if err != nil { 66 | log.Print(err) 67 | } 68 | } 69 | 70 | func (h *Http) Stop() { 71 | } 72 | 73 | func index(w http.ResponseWriter, r *http.Request) { 74 | routes.ServeJson(w, httpInstance.bitTorrent.GetTorrentInfos()) 75 | } 76 | 77 | func video(w http.ResponseWriter, r *http.Request) { 78 | magnetLink := getQueryParam(r, "magnet_link", "") 79 | downloadDir := getQueryParam(r, "download_dir", ".") 80 | preview := getQueryParam(r, "preview", "0") 81 | lookAhead, _ := strconv.ParseFloat(getQueryParam(r, "look_ahead", "0.005"), 32) 82 | mixpanelData := getQueryParam(r, "mixpanel_data", "") 83 | 84 | if magnetLink != "" { 85 | if regExpMatch := regexp.MustCompile(`xt=urn:btih:([a-zA-Z0-9]+)`).FindStringSubmatch(magnetLink); len(regExpMatch) == 2 { 86 | infoHash := strings.ToUpper(regExpMatch[1]) 87 | 88 | httpInstance.bitTorrent.AddTorrent(magnetLink, downloadDir, infoHash, float32(lookAhead), mixpanelData) 89 | 90 | if torrentInfo := httpInstance.bitTorrent.GetTorrentInfo(infoHash); torrentInfo != nil { 91 | httpInstance.bitTorrent.AddConnection(infoHash) 92 | defer httpInstance.bitTorrent.RemoveConnection(infoHash) 93 | 94 | if torrentFileInfo := torrentInfo.GetBiggestTorrentFileInfo(); torrentFileInfo != nil { 95 | if preview == "0" { 96 | if torrentFileInfo.Open(torrentInfo.DownloadDir) { 97 | defer torrentFileInfo.Close() 98 | http.ServeContent(w, r, torrentFileInfo.Path, time.Time{}, torrentFileInfo) 99 | } else { 100 | http.Error(w, "Failed to open file", http.StatusInternalServerError) 101 | } 102 | } else { 103 | videoReady(w, torrentFileInfo.IsVideoReady()) 104 | } 105 | } else { 106 | // Video not ready yet 107 | if preview == "0" { 108 | redirect(w, r) 109 | } else { 110 | videoReady(w, false) 111 | } 112 | } 113 | } else { 114 | // Torrent not ready yet 115 | if preview == "0" { 116 | redirect(w, r) 117 | } else { 118 | videoReady(w, false) 119 | } 120 | } 121 | } else { 122 | http.Error(w, "Invalid Magnet link", http.StatusBadRequest) 123 | } 124 | } else { 125 | http.Error(w, "Missing Magnet link", http.StatusBadRequest) 126 | } 127 | } 128 | 129 | func shutdown(w http.ResponseWriter, r *http.Request) { 130 | w.WriteHeader(http.StatusOK) 131 | httpInstance.server.Stop(500 * time.Millisecond) 132 | } 133 | 134 | func getQueryParam(r *http.Request, paramName string, defaultValue string) (result string) { 135 | result = r.URL.Query().Get(paramName) 136 | if result == "" { 137 | result = defaultValue 138 | } 139 | return result 140 | } 141 | 142 | func redirect(w http.ResponseWriter, r *http.Request) { 143 | time.Sleep(2 * time.Second) 144 | http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) 145 | } 146 | 147 | func videoReady(w http.ResponseWriter, videoReady bool) { 148 | routes.ServeJson(w, map[string]interface{}{"video_ready": videoReady}) 149 | } 150 | -------------------------------------------------------------------------------- /bittorrent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "os" 8 | "path" 9 | "strings" 10 | "time" 11 | 12 | "github.com/sharkone/libtorrent-go" 13 | ) 14 | 15 | type TorrentFileInfo struct { 16 | Path string `json:"path"` 17 | Size int64 `json:"size"` 18 | CompletePieces int `json:"complete_pieces"` 19 | TotalPieces int `json:"total_pieces"` 20 | PieceMap []string `json:"piece_map"` 21 | 22 | handle libtorrent.Torrent_handle 23 | offset int64 24 | pieceLength int 25 | startPiece int 26 | endPiece int 27 | 28 | file *os.File 29 | bytesRead int 30 | } 31 | 32 | func NewTorrentFileInfo(path string, size int64, offset int64, pieceLength int, handle libtorrent.Torrent_handle) *TorrentFileInfo { 33 | result := &TorrentFileInfo{} 34 | result.Path = path 35 | result.Size = size 36 | result.offset = offset 37 | result.pieceLength = pieceLength 38 | result.handle = handle 39 | result.startPiece = result.GetPieceIndexFromOffset(0) 40 | result.endPiece = result.GetPieceIndexFromOffset(size) 41 | result.CompletePieces = result.GetCompletePieces() 42 | result.TotalPieces = 1 + result.endPiece - result.startPiece 43 | result.PieceMap = result.GetPieceMap() 44 | return result 45 | } 46 | 47 | func (tfi *TorrentFileInfo) GetInfoHashStr() string { 48 | return bitTorrent.getTorrentInfoHash(tfi.handle) 49 | } 50 | 51 | func (tfi *TorrentFileInfo) GetPieceIndexFromOffset(offset int64) int { 52 | pieceIndex := int((tfi.offset + offset) / int64(tfi.pieceLength)) 53 | return pieceIndex 54 | } 55 | 56 | func (tfi *TorrentFileInfo) GetCompletePieces() int { 57 | completePieces := 0 58 | for i := tfi.startPiece; i <= tfi.endPiece; i++ { 59 | if tfi.handle.Have_piece(i) { 60 | completePieces += 1 61 | } 62 | } 63 | return completePieces 64 | } 65 | 66 | func (tfi *TorrentFileInfo) GetPieceMap() []string { 67 | totalRows := tfi.TotalPieces / 100 68 | if (tfi.TotalPieces % 100) != 0 { 69 | totalRows++ 70 | } 71 | 72 | result := make([]string, totalRows) 73 | for i := tfi.startPiece; i <= tfi.endPiece; i++ { 74 | if tfi.handle.Have_piece(i) { 75 | result[(i-tfi.startPiece)/100] += "*" 76 | } else { 77 | result[(i-tfi.startPiece)/100] += fmt.Sprintf("%v", tfi.handle.Piece_priority(i)) 78 | } 79 | } 80 | return result 81 | } 82 | 83 | func (tfi *TorrentFileInfo) SetInitialPriority() { 84 | start := tfi.startPiece 85 | end := int(math.Min(float64(start+tfi.getLookAhead(true)), float64(tfi.endPiece))) 86 | for i := start; i <= end; i++ { 87 | tfi.handle.Set_piece_deadline(i, 10000, 0) 88 | } 89 | 90 | tfi.handle.Set_piece_deadline(tfi.endPiece, 10000, 0) 91 | } 92 | 93 | func (tfi *TorrentFileInfo) IsVideoReady() bool { 94 | start := tfi.startPiece 95 | end := int(math.Min(float64(start+tfi.getLookAhead(true)), float64(tfi.endPiece))) 96 | for i := start; i <= end; i++ { 97 | if !tfi.handle.Have_piece(i) { 98 | return false 99 | } 100 | } 101 | 102 | if !tfi.handle.Have_piece(tfi.endPiece) { 103 | return false 104 | } 105 | 106 | return true 107 | } 108 | 109 | func (tfi *TorrentFileInfo) Open(downloadDir string) bool { 110 | if tfi.file == nil { 111 | fullpath := path.Join(downloadDir, tfi.Path) 112 | 113 | for { 114 | if _, err := os.Stat(fullpath); err == nil { 115 | break 116 | } 117 | time.Sleep(100 * time.Millisecond) 118 | } 119 | 120 | tfi.file, _ = os.Open(fullpath) 121 | tfi.bytesRead = 0 122 | } 123 | 124 | return tfi.file != nil 125 | } 126 | 127 | func (tfi *TorrentFileInfo) Close() { 128 | if tfi.file != nil { 129 | tfi.file.Close() 130 | } 131 | } 132 | 133 | func (tfi *TorrentFileInfo) Read(data []byte) (int, error) { 134 | totalRead := 0 135 | size := len(data) 136 | 137 | for size > 0 { 138 | readSize := int64(math.Min(float64(size), float64(tfi.pieceLength))) 139 | 140 | currentPosition, _ := tfi.file.Seek(0, os.SEEK_CUR) 141 | pieceIndex := tfi.GetPieceIndexFromOffset(currentPosition + readSize) 142 | tfi.waitForPiece(pieceIndex, false) 143 | 144 | tmpData := make([]byte, readSize) 145 | read, err := tfi.file.Read(tmpData) 146 | if err != nil { 147 | totalRead += read 148 | log.Println("[scrapmagnet] Read failed", read, readSize, currentPosition, err) 149 | return totalRead, err 150 | } 151 | 152 | copy(data[totalRead:], tmpData[:read]) 153 | totalRead += read 154 | size -= read 155 | } 156 | 157 | tfi.bytesRead += totalRead 158 | 159 | if tfi.bytesRead > (10*1024*1024) && !bitTorrent.connectionInfos[tfi.GetInfoHashStr()].Served { 160 | log.Printf("[scrapmagnet] Serving %v", tfi.handle.Status().GetName()) 161 | trackingEvent("Serving", map[string]interface{}{"Magnet InfoHash": tfi.GetInfoHashStr(), "Magnet Name": tfi.handle.Status().GetName()}, bitTorrent.mixpanelData[tfi.GetInfoHashStr()]) 162 | bitTorrent.connectionInfos[tfi.GetInfoHashStr()].Served = true 163 | } 164 | 165 | return totalRead, nil 166 | } 167 | 168 | func (tfi *TorrentFileInfo) Seek(offset int64, whence int) (int64, error) { 169 | newPosition := int64(0) 170 | 171 | switch whence { 172 | case os.SEEK_SET: 173 | newPosition = offset 174 | case os.SEEK_CUR: 175 | currentPosition, _ := tfi.file.Seek(0, os.SEEK_CUR) 176 | newPosition = currentPosition + offset 177 | case os.SEEK_END: 178 | newPosition = tfi.Size + offset 179 | } 180 | 181 | pieceIndex := tfi.GetPieceIndexFromOffset(newPosition) 182 | tfi.waitForPiece(pieceIndex, true) 183 | 184 | ret, err := tfi.file.Seek(offset, whence) 185 | if err != nil || ret != newPosition { 186 | log.Print("[scrapmagnet] Seek failed", ret, newPosition, err) 187 | } 188 | 189 | return ret, err 190 | } 191 | 192 | func (tfi *TorrentFileInfo) waitForPiece(pieceIndex int, timeCritical bool) bool { 193 | if !tfi.handle.Have_piece(pieceIndex) { 194 | if timeCritical { 195 | tfi.handle.Clear_piece_deadlines() 196 | for i := 0; i <= tfi.getLookAhead(false) && (i+pieceIndex) <= tfi.endPiece; i++ { 197 | tfi.handle.Set_piece_deadline(pieceIndex+i, 3000+i*1000, 0) 198 | } 199 | } else { 200 | for i := tfi.startPiece; i < tfi.endPiece; i++ { 201 | tfi.handle.Piece_priority(i, 1) 202 | } 203 | for i := 0; i <= tfi.getLookAhead(false)*4 && (i+pieceIndex) <= tfi.endPiece; i++ { 204 | tfi.handle.Piece_priority(pieceIndex+i, 7) 205 | } 206 | } 207 | 208 | tfi.SetInitialPriority() 209 | 210 | for { 211 | time.Sleep(100 * time.Millisecond) 212 | if tfi.handle.Have_piece(pieceIndex) { 213 | break 214 | } 215 | } 216 | } 217 | 218 | return false 219 | } 220 | 221 | func (tfi *TorrentFileInfo) getLookAhead(initial bool) int { 222 | if initial { 223 | return int(float32(tfi.TotalPieces) * bitTorrent.lookAhead[tfi.GetInfoHashStr()]) 224 | } 225 | return int(float32(tfi.TotalPieces) * 0.005) 226 | } 227 | 228 | type TorrentInfo struct { 229 | Name string `json:"name"` 230 | InfoHash string `json:"info_hash"` 231 | DownloadDir string `json:"download_dir"` 232 | State int `json:"state"` 233 | StateStr string `json:"state_str"` 234 | Paused bool `json:"paused"` 235 | Size int64 `json:"size"` 236 | Pieces int `json:"pieces"` 237 | Progress float32 `json:"progress"` 238 | DownloadRate int `json:"download_rate"` 239 | UploadRate int `json:"upload_rate"` 240 | Seeds int `json:"seeds"` 241 | TotalSeeds int `json:"total_seeds"` 242 | Peers int `json:"peers"` 243 | TotalPeers int `json:"total_peers"` 244 | Files []*TorrentFileInfo `json:"files"` 245 | 246 | ConnectionInfo *TorrentConnectionInfo `json:"connection_info"` 247 | } 248 | 249 | func NewTorrentInfo(handle libtorrent.Torrent_handle) (result *TorrentInfo) { 250 | result = &TorrentInfo{} 251 | 252 | torrentStatus := handle.Status() 253 | 254 | result.InfoHash = bitTorrent.getTorrentInfoHash(handle) 255 | result.Name = torrentStatus.GetName() 256 | result.DownloadDir = torrentStatus.GetSave_path() 257 | result.State = int(torrentStatus.GetState()) 258 | result.StateStr = func(state libtorrent.LibtorrentTorrent_statusState_t) string { 259 | switch state { 260 | case libtorrent.Torrent_statusQueued_for_checking: 261 | return "Queued for checking" 262 | case libtorrent.Torrent_statusChecking_files: 263 | return "Checking files" 264 | case libtorrent.Torrent_statusDownloading_metadata: 265 | return "Downloading metadata" 266 | case libtorrent.Torrent_statusDownloading: 267 | return "Downloading" 268 | case libtorrent.Torrent_statusFinished: 269 | return "Finished" 270 | case libtorrent.Torrent_statusSeeding: 271 | return "Seeding" 272 | case libtorrent.Torrent_statusAllocating: 273 | return "Allocating" 274 | case libtorrent.Torrent_statusChecking_resume_data: 275 | return "Checking resume data" 276 | default: 277 | return "Unknown" 278 | } 279 | }(torrentStatus.GetState()) 280 | result.Paused = torrentStatus.GetPaused() 281 | result.Progress = torrentStatus.GetProgress() 282 | result.DownloadRate = torrentStatus.GetDownload_rate() / 1024 283 | result.UploadRate = torrentStatus.GetUpload_rate() / 1024 284 | result.Seeds = torrentStatus.GetNum_seeds() 285 | result.TotalSeeds = torrentStatus.GetNum_complete() 286 | result.Peers = torrentStatus.GetNum_peers() 287 | result.TotalPeers = torrentStatus.GetNum_incomplete() 288 | 289 | torrentInfo := handle.Torrent_file() 290 | if torrentInfo.Swigcptr() != 0 { 291 | result.Files = func(torrentInfo libtorrent.Torrent_info) (result []*TorrentFileInfo) { 292 | for i := 0; i < torrentInfo.Files().Num_files(); i++ { 293 | result = append(result, NewTorrentFileInfo(torrentInfo.Files().File_path(i), torrentInfo.Files().File_size(i), torrentInfo.Files().File_offset(i), torrentInfo.Piece_length(), handle)) 294 | } 295 | return result 296 | }(torrentInfo) 297 | result.Size = torrentInfo.Files().Total_size() 298 | result.Pieces = torrentInfo.Num_pieces() 299 | } 300 | 301 | result.ConnectionInfo = bitTorrent.connectionInfos[result.InfoHash] 302 | return result 303 | } 304 | 305 | func (ti *TorrentInfo) GetTorrentFileInfo(filePath string) *TorrentFileInfo { 306 | for _, torrentFileInfo := range ti.Files { 307 | if torrentFileInfo.Path == filePath { 308 | return torrentFileInfo 309 | } 310 | } 311 | return nil 312 | } 313 | 314 | func (ti *TorrentInfo) GetBiggestTorrentFileInfo() (result *TorrentFileInfo) { 315 | for _, torrentFileInfo := range ti.Files { 316 | if result == nil || torrentFileInfo.Size > result.Size { 317 | result = torrentFileInfo 318 | } 319 | } 320 | return result 321 | } 322 | 323 | type TorrentConnectionInfo struct { 324 | connectionChan chan int 325 | ConnectionCount int `json:"connection_count"` 326 | Served bool `json:"served"` 327 | paused bool 328 | } 329 | 330 | func NewTorrentConnectionInfo() *TorrentConnectionInfo { 331 | return &TorrentConnectionInfo{ 332 | connectionChan: make(chan int), 333 | ConnectionCount: 0, 334 | Served: false, 335 | paused: false, 336 | } 337 | } 338 | 339 | type BitTorrent struct { 340 | session libtorrent.Session 341 | lookAhead map[string]float32 342 | mixpanelData map[string]string 343 | connectionInfos map[string]*TorrentConnectionInfo 344 | removeChan chan bool 345 | deleteChan chan bool 346 | } 347 | 348 | func NewBitTorrent() *BitTorrent { 349 | return &BitTorrent{ 350 | lookAhead: make(map[string]float32), 351 | mixpanelData: make(map[string]string), 352 | connectionInfos: make(map[string]*TorrentConnectionInfo), 353 | removeChan: make(chan bool), 354 | deleteChan: make(chan bool), 355 | } 356 | } 357 | 358 | func (b *BitTorrent) Start() { 359 | peopleSet() 360 | 361 | fingerprint := libtorrent.NewFingerprint("LT", libtorrent.LIBTORRENT_VERSION_MAJOR, libtorrent.LIBTORRENT_VERSION_MINOR, 0, 0) 362 | sessionFlags := int(libtorrent.SessionAdd_default_plugins) 363 | alertMask := uint(libtorrent.AlertError_notification | libtorrent.AlertStorage_notification | libtorrent.AlertStatus_notification) 364 | 365 | b.session = libtorrent.NewSession(fingerprint, sessionFlags) 366 | b.session.Set_alert_mask(alertMask) 367 | go b.alertPump() 368 | 369 | sessionSettings := b.session.Settings() 370 | sessionSettings.SetAnnounce_to_all_tiers(true) 371 | sessionSettings.SetAnnounce_to_all_trackers(true) 372 | sessionSettings.SetConnection_speed(100) 373 | sessionSettings.SetPeer_connect_timeout(2) 374 | sessionSettings.SetRate_limit_ip_overhead(true) 375 | sessionSettings.SetRequest_timeout(5) 376 | sessionSettings.SetTorrent_connect_boost(100) 377 | if settings.maxDownloadRate > 0 { 378 | sessionSettings.SetDownload_rate_limit(settings.maxDownloadRate * 1024) 379 | } 380 | if settings.maxUploadRate > 0 { 381 | sessionSettings.SetUpload_rate_limit(settings.maxUploadRate * 1024) 382 | } 383 | b.session.Set_settings(sessionSettings) 384 | 385 | proxySettings := libtorrent.NewProxy_settings() 386 | if settings.proxyType == "SOCKS5" { 387 | proxySettings.SetHostname(settings.proxyHost) 388 | proxySettings.SetPort(uint16(settings.proxyPort)) 389 | if settings.proxyUser != "" { 390 | proxySettings.SetXtype(byte(libtorrent.Proxy_settingsSocks5_pw)) 391 | proxySettings.SetUsername(settings.proxyUser) 392 | proxySettings.SetPassword(settings.proxyPassword) 393 | } else { 394 | proxySettings.SetXtype(byte(libtorrent.Proxy_settingsSocks5)) 395 | } 396 | } 397 | b.session.Set_proxy(proxySettings) 398 | 399 | encryptionSettings := libtorrent.NewPe_settings() 400 | encryptionSettings.SetOut_enc_policy(byte(libtorrent.Pe_settingsForced)) 401 | encryptionSettings.SetIn_enc_policy(byte(libtorrent.Pe_settingsForced)) 402 | encryptionSettings.SetAllowed_enc_level(byte(libtorrent.Pe_settingsBoth)) 403 | encryptionSettings.SetPrefer_rc4(true) 404 | b.session.Set_pe_settings(encryptionSettings) 405 | 406 | ec := libtorrent.NewError_code() 407 | b.session.Listen_on(libtorrent.NewStd_pair_int_int(settings.bitTorrentPort, settings.bitTorrentPort), ec) 408 | 409 | b.session.Start_dht() 410 | b.session.Start_lsd() 411 | 412 | if settings.uPNPNatPMPEnabled { 413 | b.session.Start_upnp() 414 | b.session.Start_natpmp() 415 | } 416 | } 417 | 418 | func (b *BitTorrent) Stop() { 419 | for i := 0; i < int(b.session.Get_torrents().Size()); i++ { 420 | b.removeTorrent(b.session.Get_torrents().Get(i)) 421 | } 422 | 423 | if settings.uPNPNatPMPEnabled { 424 | b.session.Stop_natpmp() 425 | b.session.Stop_upnp() 426 | } 427 | 428 | b.session.Stop_lsd() 429 | b.session.Stop_dht() 430 | } 431 | 432 | func (b *BitTorrent) AddTorrent(magnetLink string, downloadDir string, infoHash string, lookAhead float32, mixpanelData string) { 433 | addTorrentParams := libtorrent.NewAdd_torrent_params() 434 | addTorrentParams.SetUrl(magnetLink) 435 | addTorrentParams.SetSave_path(downloadDir) 436 | addTorrentParams.SetStorage_mode(libtorrent.Storage_mode_sparse) 437 | addTorrentParams.SetFlags(0) 438 | 439 | if _, ok := b.lookAhead[infoHash]; !ok { 440 | b.lookAhead[infoHash] = lookAhead 441 | } 442 | 443 | if _, ok := b.mixpanelData[infoHash]; !ok { 444 | b.mixpanelData[infoHash] = mixpanelData 445 | } 446 | 447 | b.session.Async_add_torrent(addTorrentParams) 448 | } 449 | 450 | func (b *BitTorrent) GetTorrentInfos() (result []*TorrentInfo) { 451 | result = make([]*TorrentInfo, 0, 0) 452 | handles := b.session.Get_torrents() 453 | for i := 0; i < int(handles.Size()); i++ { 454 | if _, ok := b.connectionInfos[b.getTorrentInfoHash(handles.Get(i))]; ok { 455 | result = append(result, NewTorrentInfo(handles.Get(i))) 456 | } 457 | } 458 | return result 459 | } 460 | 461 | func (b *BitTorrent) GetTorrentInfo(infoHash string) *TorrentInfo { 462 | handles := b.session.Get_torrents() 463 | for i := 0; i < int(handles.Size()); i++ { 464 | if infoHash == b.getTorrentInfoHash(handles.Get(i)) { 465 | if _, ok := b.connectionInfos[infoHash]; ok { 466 | return NewTorrentInfo(handles.Get(i)) 467 | } 468 | } 469 | } 470 | return nil 471 | } 472 | 473 | func (b *BitTorrent) AddConnection(infoHash string) { 474 | b.connectionInfos[infoHash].connectionChan <- 1 475 | } 476 | 477 | func (b *BitTorrent) RemoveConnection(infoHash string) { 478 | b.connectionInfos[infoHash].connectionChan <- -1 479 | } 480 | 481 | func (b *BitTorrent) getTorrentInfoHash(handle libtorrent.Torrent_handle) string { 482 | return fmt.Sprintf("%X", handle.Info_hash().To_string()) 483 | } 484 | 485 | func (b *BitTorrent) pauseTorrent(handle libtorrent.Torrent_handle) { 486 | handle.Pause() 487 | } 488 | 489 | func (b *BitTorrent) resumeTorrent(handle libtorrent.Torrent_handle) { 490 | handle.Resume() 491 | } 492 | 493 | func (b *BitTorrent) removeTorrent(handle libtorrent.Torrent_handle) { 494 | removeFlags := 0 495 | if !settings.keepFiles { 496 | removeFlags |= int(libtorrent.SessionDelete_files) 497 | } 498 | 499 | b.session.Remove_torrent(handle, removeFlags) 500 | <-b.removeChan 501 | 502 | if (removeFlags & int(libtorrent.SessionDelete_files)) != 0 { 503 | <-b.deleteChan 504 | } 505 | } 506 | 507 | func (b *BitTorrent) alertPump() { 508 | for { 509 | if b.session.Wait_for_alert(libtorrent.Seconds(1)).Swigcptr() != 0 { 510 | alert := b.session.Pop_alert() 511 | switch alert.Xtype() { 512 | case libtorrent.Torrent_added_alertAlert_type: 513 | torrentAddedAlert := libtorrent.SwigcptrTorrent_added_alert(alert.Swigcptr()) 514 | b.onTorrentAdded(torrentAddedAlert.GetHandle()) 515 | case libtorrent.Metadata_received_alertAlert_type: 516 | metadataReceivedAlert := libtorrent.SwigcptrMetadata_received_alert(alert.Swigcptr()) 517 | b.onMetadataReceived(metadataReceivedAlert.GetHandle()) 518 | case libtorrent.Torrent_paused_alertAlert_type: 519 | torrentPausedAlert := libtorrent.SwigcptrTorrent_paused_alert(alert.Swigcptr()) 520 | b.onTorrentPaused(torrentPausedAlert.GetHandle()) 521 | case libtorrent.Torrent_resumed_alertAlert_type: 522 | torrentResumedAlert := libtorrent.SwigcptrTorrent_resumed_alert(alert.Swigcptr()) 523 | b.onTorrentResumed(torrentResumedAlert.GetHandle()) 524 | case libtorrent.Torrent_finished_alertAlert_type: 525 | torrentFinishedAlert := libtorrent.SwigcptrTorrent_finished_alert(alert.Swigcptr()) 526 | b.onTorrentFinished(torrentFinishedAlert.GetHandle()) 527 | case libtorrent.Torrent_removed_alertAlert_type: 528 | torrentRemovedAlert := libtorrent.SwigcptrTorrent_removed_alert(alert.Swigcptr()) 529 | b.onTorrentRemoved(torrentRemovedAlert.GetHandle()) 530 | case libtorrent.Torrent_deleted_alertAlert_type: 531 | torrentDeletedAlert := libtorrent.SwigcptrTorrent_deleted_alert(alert.Swigcptr()) 532 | b.onTorrentDeleted(torrentDeletedAlert.GetInfo_hash().To_string(), true) 533 | case libtorrent.Torrent_delete_failed_alertAlert_type: 534 | torrentDeletedAlert := libtorrent.SwigcptrTorrent_deleted_alert(alert.Swigcptr()) 535 | b.onTorrentDeleted(torrentDeletedAlert.GetInfo_hash().To_string(), false) 536 | case libtorrent.Listen_succeeded_alertAlert_type: 537 | listenSucceedAlert := libtorrent.SwigcptrListen_succeeded_alert(alert.Swigcptr()) 538 | if listenSucceedAlert.GetSock_type() != libtorrent.Listen_succeeded_alertTcp_ssl && !strings.Contains(listenSucceedAlert.Message(), "[::]") { 539 | log.Printf("[scrapmagnet] %s", listenSucceedAlert.Message()) 540 | } 541 | case libtorrent.Add_torrent_alertAlert_type: 542 | // Ignore 543 | case libtorrent.Torrent_checked_alertAlert_type: 544 | // Ignore 545 | case libtorrent.State_changed_alertAlert_type: 546 | // Ignore 547 | case libtorrent.Hash_failed_alertAlert_type: 548 | // Ignore 549 | case libtorrent.Cache_flushed_alertAlert_type: 550 | // Ignore 551 | case libtorrent.External_ip_alertAlert_type: 552 | // Ignore 553 | case libtorrent.Portmap_error_alertAlert_type: 554 | // Ignore 555 | case libtorrent.Tracker_error_alertAlert_type: 556 | // Ignore 557 | case libtorrent.Udp_error_alertAlert_type: 558 | // Ignore 559 | default: 560 | log.Printf("[scrapmagnet] %s: %s", alert.What(), alert.Message()) 561 | } 562 | } 563 | } 564 | } 565 | 566 | func (b *BitTorrent) onTorrentAdded(handle libtorrent.Torrent_handle) { 567 | infoHash := b.getTorrentInfoHash(handle) 568 | 569 | b.connectionInfos[infoHash] = NewTorrentConnectionInfo() 570 | 571 | go func() { 572 | watcherRunning := false 573 | resumeChan := make(chan bool) 574 | 575 | // Auto pause/remove 576 | for { 577 | b.connectionInfos[infoHash].ConnectionCount += <-b.connectionInfos[infoHash].connectionChan 578 | if b.connectionInfos[infoHash].ConnectionCount > 0 { 579 | if watcherRunning { 580 | resumeChan <- true 581 | } 582 | } else { 583 | go func() { 584 | watcherRunning = true 585 | paused := false 586 | Watcher: 587 | for { 588 | if !paused { 589 | select { 590 | case <-resumeChan: 591 | break Watcher 592 | case <-time.After(time.Duration(settings.inactivityPauseTimeout) * time.Second): 593 | b.pauseTorrent(handle) 594 | paused = true 595 | } 596 | } else { 597 | select { 598 | case <-resumeChan: 599 | b.resumeTorrent(handle) 600 | break Watcher 601 | case <-time.After(time.Duration(settings.inactivityRemoveTimeout) * time.Second): 602 | b.removeTorrent(handle) 603 | break Watcher 604 | } 605 | } 606 | } 607 | watcherRunning = false 608 | }() 609 | } 610 | } 611 | }() 612 | 613 | log.Printf("[scrapmagnet] Added %v", handle.Status().GetName()) 614 | trackingEvent("Added", map[string]interface{}{"Magnet InfoHash": b.getTorrentInfoHash(handle), "Magnet Name": handle.Status().GetName()}, b.mixpanelData[infoHash]) 615 | } 616 | 617 | func (b *BitTorrent) onMetadataReceived(handle libtorrent.Torrent_handle) { 618 | torrentInfo := b.GetTorrentInfo(b.getTorrentInfoHash(handle)) 619 | for i := 0; i < len(torrentInfo.Files); i++ { 620 | torrentInfo.Files[i].SetInitialPriority() 621 | } 622 | 623 | log.Printf("[scrapmagnet] Metadata received %v", handle.Status().GetName()) 624 | trackingEvent("Metadata received", map[string]interface{}{"Magnet InfoHash": b.getTorrentInfoHash(handle), "Magnet Name": handle.Status().GetName()}, b.mixpanelData[b.getTorrentInfoHash(handle)]) 625 | } 626 | 627 | func (b *BitTorrent) onTorrentPaused(handle libtorrent.Torrent_handle) { 628 | if !b.connectionInfos[b.getTorrentInfoHash(handle)].paused { 629 | log.Printf("[scrapmagnet] Paused %v", handle.Status().GetName()) 630 | b.connectionInfos[b.getTorrentInfoHash(handle)].paused = true 631 | } 632 | } 633 | 634 | func (b *BitTorrent) onTorrentResumed(handle libtorrent.Torrent_handle) { 635 | if b.connectionInfos[b.getTorrentInfoHash(handle)].paused { 636 | log.Printf("[scrapmagnet] Resumed %v", handle.Status().GetName()) 637 | b.connectionInfos[b.getTorrentInfoHash(handle)].paused = false 638 | } 639 | } 640 | 641 | func (b *BitTorrent) onTorrentFinished(handle libtorrent.Torrent_handle) { 642 | log.Printf("[scrapmagnet] Finished %v", handle.Status().GetName()) 643 | trackingEvent("Finished", map[string]interface{}{"Magnet InfoHash": b.getTorrentInfoHash(handle), "Magnet Name": handle.Status().GetName()}, b.mixpanelData[b.getTorrentInfoHash(handle)]) 644 | } 645 | 646 | func (b *BitTorrent) onTorrentRemoved(handle libtorrent.Torrent_handle) { 647 | log.Printf("[scrapmagnet] Removed %v", handle.Status().GetName()) 648 | trackingEvent("Removed", map[string]interface{}{"Magnet InfoHash": b.getTorrentInfoHash(handle), "Magnet Name": handle.Status().GetName()}, b.mixpanelData[b.getTorrentInfoHash(handle)]) 649 | 650 | delete(b.mixpanelData, b.getTorrentInfoHash(handle)) 651 | delete(b.lookAhead, b.getTorrentInfoHash(handle)) 652 | delete(b.connectionInfos, b.getTorrentInfoHash(handle)) 653 | b.removeChan <- true 654 | } 655 | 656 | func (b *BitTorrent) onTorrentDeleted(infoHash string, success bool) { 657 | b.deleteChan <- success 658 | 659 | { 660 | if success { 661 | log.Printf("[scrapmagnet] Deleted") 662 | } else { 663 | log.Printf("[scrapmagnet] Delete failed") 664 | } 665 | } 666 | } 667 | --------------------------------------------------------------------------------