+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 %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,
+ humanize.IBytes(uint64(stats.BytesReadData.Int64())),
+ humanize.IBytes(uint64(stats.BytesWrittenData.Int64())),
+ 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))
+}