package main import ( "bufio" "bytes" "encoding/hex" "fmt" "log" "os" "path" "sort" "strconv" "strings" "sync" "syscall" "time" "github.com/anacrolix/dht/v2" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/storage" "github.com/anacrolix/torrent/types/infohash" "github.com/dustin/go-humanize" ) const ( MaxListNameWidth = 40 PeersDir = "peers" FilesDir = "files" ) type TorrentStat struct { stats torrent.ConnStats rxSpeed int64 txSpeed int64 } var ( FIFOsDir = "fifos" TorrentStats = map[metainfo.Hash]TorrentStat{} TorrentStatsM sync.RWMutex Torrents []metainfo.Hash TorrentsM sync.RWMutex ) func recreateFIFO(pth string) { os.Remove(pth) if err := syscall.Mkfifo(pth, 0666); err != nil { log.Fatalln(err) } } func shortenName(name string) string { s := []rune(name) if len(s) > MaxListNameWidth { s = s[:MaxListNameWidth] } return string(s) } func fifoList(c *torrent.Client) { pth := path.Join(FIFOsDir, "list") recreateFIFO(pth) for { fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) if err != nil { log.Println("OpenFile:", pth, err) time.Sleep(time.Second) continue } ts := make([]*torrent.Torrent, 0, len(Torrents)) TorrentsM.RLock() for _, h := range Torrents { t, _ := c.Torrent(h) if t == nil || t.Info() == nil { fmt.Fprintf(fd, "%s not ready\n", t.Name()) continue } ts = append(ts, t) } TorrentsM.RUnlock() for _, t := range ts { stats := t.Stats() done := t.BytesCompleted() * 100 / t.Length() percColour := Red if done == 100 { percColour = Green } tx := stats.BytesWrittenData.Int64() tx += TxStats[t.InfoHash()] ratio := float64(tx) / float64(t.Length()) TorrentStatsM.RLock() prev := TorrentStats[t.InfoHash()] TorrentStatsM.RUnlock() var eta string if done < 100 && prev.rxSpeed > 0 { etaRaw := time.Duration((t.Length() - t.BytesCompleted()) / prev.rxSpeed) etaRaw *= time.Second eta = etaRaw.String() } fmt.Fprintf(fd, "%s%s%s %s%40s%s %8s %s%3d%%%s %4.1f %s%d%s/%s%d%s %d/%d/%d/%d %s\n", Blue, t.InfoHash().HexString(), Reset, Green, shortenName(t.Name()), Reset, humanize.IBytes(uint64(t.Length())), percColour, done, Reset, ratio, Green, prev.rxSpeed/1024, Reset, Magenta, prev.txSpeed/1024, Reset, stats.TotalPeers, stats.PendingPeers, stats.ActivePeers, stats.ConnectedSeeders, eta, ) } fd.Close() time.Sleep(time.Second) } } func mustParseInt(s string) int { i, err := strconv.Atoi(s) if err != nil { log.Fatalln(err) } return i } func fifoPeerList(t *torrent.Torrent) { pth := path.Join(FIFOsDir, PeersDir, t.InfoHash().HexString()) recreateFIFO(pth) for { fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) if err != nil { if os.IsNotExist(err) { break } log.Println("OpenFile:", pth, err) time.Sleep(time.Second) continue } pcs := t.PeerConns() sort.Sort(ByPeerID(pcs)) for _, pc := range pcs { cols := strings.Split(pc.CompletedString(), "/") done := (mustParseInt(cols[0]) * 100) / mustParseInt(cols[1]) doneColour := Red if done == 100 { doneColour = Green } stats := pc.Peer.Stats() fmt.Fprintf(fd, "%s%s%s %10s %s%3d%%%s %s%d%s/%s%d%s | %s%s%s / %s%s%s | %s%s%s %q\n", Blue, hex.EncodeToString(pc.PeerID[:]), Reset, pc.StatusFlags(), doneColour, done, Reset, Green, int(pc.DownloadRate()/1024), Reset, Magenta, int(pc.UploadRate()/1024), Reset, Green, humanize.IBytes(uint64(stats.BytesReadData.Int64())), Reset, Magenta, humanize.IBytes(uint64(stats.BytesWrittenData.Int64())), Reset, Green, pc.RemoteAddr, Reset, pc.PeerClientName, ) } fd.Close() time.Sleep(time.Second) } } func fifoFileList(t *torrent.Torrent) { pth := path.Join(FIFOsDir, FilesDir, t.InfoHash().HexString()) recreateFIFO(pth) for { fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) if err != nil { if os.IsNotExist(err) { break } log.Println("OpenFile:", pth, err) time.Sleep(time.Second) continue } for n, f := range t.Files() { var done int64 if f.Length() > 0 { done = (f.BytesCompleted() * 100) / f.Length() } percColour := Green if done < 100 { percColour = Red } fmt.Fprintf(fd, "%5d %8s %3d%% | %s%s%s\n", n, humanize.IBytes(uint64(f.Length())), done, percColour, f.Path(), Reset, ) } fd.Close() time.Sleep(time.Second) } } func fifoDHTList(c *torrent.Client) { pth := path.Join(FIFOsDir, "dht") recreateFIFO(pth) for { fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) if err != nil { if os.IsNotExist(err) { break } log.Println("OpenFile:", pth, err) time.Sleep(time.Second) continue } for _, s := range c.DhtServers() { stats := s.Stats().(dht.ServerStats) fmt.Fprintf( fd, "%s%s%s all:%d good:%d await:%d succ:%d bad:%d\n", Green, s.Addr().String(), Reset, stats.Nodes, stats.GoodNodes, stats.OutstandingTransactions, stats.SuccessfulOutboundAnnouncePeerQueries, stats.BadNodes, ) } fd.Close() time.Sleep(time.Second) } } type topTorrent struct { infoHash metainfo.Hash name string tx int64 ratio float64 } func fifoTopSeed(c *torrent.Client) { pth := path.Join(FIFOsDir, "top-seed") recreateFIFO(pth) for { fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) if err != nil { log.Println("OpenFile:", pth, err) time.Sleep(time.Second) continue } var ts []*topTorrent for _, t := range c.Torrents() { if t.Info() == nil { continue } stats := t.Stats() top := topTorrent{ infoHash: t.InfoHash(), name: t.Name(), tx: stats.BytesWrittenData.Int64() + TxStats[t.InfoHash()], } top.ratio = float64(top.tx) / float64(t.Length()) ts = append(ts, &top) } sort.Sort(ByTxTraffic(ts)) for _, t := range ts { fmt.Fprintf(fd, "%s%s%s %s%40s%s %s%4.1f%s %s\n", Blue, t.infoHash.HexString(), Reset, Green, shortenName(t.name), Reset, Magenta, t.ratio, Reset, humanize.IBytes(uint64(t.tx)), ) } fd.Close() time.Sleep(time.Second) } } type stringAddr string func (stringAddr) Network() string { return "" } func (me stringAddr) String() string { return string(me) } func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) { for _, ta := range addrs { ret = append(ret, torrent.PeerInfo{Addr: stringAddr(ta)}) } return } func readLinesFromFIFO(pth string) []string { fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666)) if err != nil { log.Println("OpenFile:", pth, err) time.Sleep(time.Second) return nil } var lines []string scanner := bufio.NewScanner(fd) for scanner.Scan() { t := scanner.Text() if len(t) > 0 { lines = append(lines, t) } } fd.Close() return lines } func saveTorrent(t *torrent.Torrent) error { pth := storage.PathShortener(t.Name()) + TorrentExt if _, err := os.Stat(pth); err == nil { return nil } var b bytes.Buffer t.Metainfo().Write(&b) return os.WriteFile(pth, b.Bytes(), 0666) } func fifoAdd(c *torrent.Client) { pth := path.Join(FIFOsDir, "add") recreateFIFO(pth) for { for _, what := range readLinesFromFIFO(pth) { cols := strings.Fields(what) what = cols[0] var t *torrent.Torrent var err error if strings.HasPrefix(what, "magnet:") { t, err = c.AddMagnet(what) if err != nil { log.Println("AddMagnet:", what, err) continue } } else { metaInfo, err := metainfo.LoadFromFile(what) if err != nil { log.Println("LoadFromFile:", what, err) continue } t, err = c.AddTorrent(metaInfo) if err != nil { log.Println("AddTorrent:", what, err) continue } } if len(cols) > 1 { t.AddPeers(resolveTestPeers(cols[1:])) } TorrentsM.Lock() for _, h := range Torrents { if h.HexString() == t.InfoHash().HexString() { goto OldOne } } Torrents = append(Torrents, t.InfoHash()) OldOne: TorrentsM.Unlock() go fifoPeerList(t) go fifoFileList(t) log.Println("added:", t.InfoHash().HexString(), t.Name()) go func() { <-t.GotInfo() if err = saveTorrent(t); err != nil { log.Println("saveTorrent:", err) } txStatsLoad(t.InfoHash()) t.DownloadAll() }() } time.Sleep(time.Second) } } func fifoDel(c *torrent.Client) { pth := path.Join(FIFOsDir, "del") recreateFIFO(pth) for { for _, what := range readLinesFromFIFO(pth) { raw, err := hex.DecodeString(what) if err != nil { log.Println(err) continue } if len(raw) != infohash.Size { log.Println("bad length") continue } var i infohash.T copy(i[:], raw) TorrentsM.Lock() for n, h := range Torrents { if h.HexString() == i.HexString() { Torrents = append(Torrents[:n], Torrents[n+1:]...) break } } TorrentsM.Unlock() t, ok := c.Torrent(i) if !ok { log.Println("no such torrent", what) continue } txStatsDump(t) txStatsDel(t.InfoHash()) t.Drop() for _, where := range []string{"files", "peers"} { pth := path.Join(where, t.InfoHash().HexString()) os.Remove(pth) fd, err := os.Open(pth) if err == nil { fd.Close() } } log.Println("deleted:", what, t.Name()) } time.Sleep(time.Second) } } func fifosPrepare() { os.MkdirAll(path.Join(FIFOsDir, PeersDir), 0777) os.MkdirAll(path.Join(FIFOsDir, FilesDir), 0777) } func fifosCleanup() { os.Remove(path.Join(FIFOsDir, "list")) os.Remove(path.Join(FIFOsDir, "dht")) os.Remove(path.Join(FIFOsDir, "add")) os.Remove(path.Join(FIFOsDir, "del")) os.Remove(path.Join(FIFOsDir, "top-seed")) os.RemoveAll(path.Join(FIFOsDir, PeersDir)) os.RemoveAll(path.Join(FIFOsDir, FilesDir)) }