github.com/anacrolix/torrent@v1.61.0/cmd/torrent-pick/main.go (about)

     1  // Downloads torrents from the command-line.
     2  package main
     3  
     4  import (
     5  	"bufio"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net"
    10  	"net/http"
    11  	_ "net/http/pprof"
    12  	"os"
    13  	"strings"
    14  	"time"
    15  
    16  	_ "github.com/anacrolix/envpprof"
    17  	"github.com/dustin/go-humanize"
    18  	"github.com/jessevdk/go-flags"
    19  
    20  	"github.com/anacrolix/torrent"
    21  	"github.com/anacrolix/torrent/metainfo"
    22  )
    23  
    24  // fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0])
    25  
    26  func resolvedPeerAddrs(ss []string) (ret []torrent.PeerInfo, err error) {
    27  	for _, s := range ss {
    28  		var addr *net.TCPAddr
    29  		addr, err = net.ResolveTCPAddr("tcp", s)
    30  		if err != nil {
    31  			return
    32  		}
    33  		ret = append(ret, torrent.PeerInfo{
    34  			Addr: addr,
    35  		})
    36  	}
    37  	return
    38  }
    39  
    40  func bytesCompleted(tc *torrent.Client) (ret int64) {
    41  	for _, t := range tc.Torrents() {
    42  		if t.Info() != nil {
    43  			ret += t.BytesCompleted()
    44  		}
    45  	}
    46  	return
    47  }
    48  
    49  // Returns an estimate of the total bytes for all torrents.
    50  func totalBytesEstimate(tc *torrent.Client) (ret int64) {
    51  	var noInfo, hadInfo int64
    52  	for _, t := range tc.Torrents() {
    53  		info := t.Info()
    54  		if info == nil {
    55  			noInfo++
    56  			continue
    57  		}
    58  		ret += info.TotalLength()
    59  		hadInfo++
    60  	}
    61  	if hadInfo != 0 {
    62  		// Treat each torrent without info as the average of those with,
    63  		// rounded up.
    64  		ret += (noInfo*ret + hadInfo - 1) / hadInfo
    65  	}
    66  	return
    67  }
    68  
    69  func progressLine(tc *torrent.Client) string {
    70  	return fmt.Sprintf("\033[K%s / %s\r", humanize.Bytes(uint64(bytesCompleted(tc))), humanize.Bytes(uint64(totalBytesEstimate(tc))))
    71  }
    72  
    73  func dstFileName(picked string) string {
    74  	parts := strings.Split(picked, "/")
    75  	return parts[len(parts)-1]
    76  }
    77  
    78  func main() {
    79  	log.SetFlags(log.LstdFlags | log.Lshortfile)
    80  	rootGroup := struct {
    81  		Client    *torrent.ClientConfig `group:"Client Options"`
    82  		TestPeers []string              `long:"test-peer" description:"address of peer to inject to every torrent"`
    83  		Pick      string                `long:"pick" description:"filename to pick"`
    84  	}{
    85  		Client: torrent.NewDefaultClientConfig(),
    86  	}
    87  	// Don't pass flags.PrintError because it's inconsistent with printing.
    88  	// https://github.com/jessevdk/go-flags/issues/132
    89  	parser := flags.NewParser(&rootGroup, flags.HelpFlag|flags.PassDoubleDash)
    90  	parser.Usage = "[OPTIONS] (magnet URI or .torrent file path)..."
    91  	posArgs, err := parser.Parse()
    92  	if err != nil {
    93  		fmt.Fprintf(os.Stderr, "%s", "Download from the BitTorrent network.\n\n")
    94  		fmt.Println(err)
    95  		os.Exit(2)
    96  	}
    97  	log.Printf("File to pick: %s", rootGroup.Pick)
    98  
    99  	testPeers, err := resolvedPeerAddrs(rootGroup.TestPeers)
   100  	if err != nil {
   101  		log.Fatal(err)
   102  	}
   103  
   104  	if len(posArgs) == 0 {
   105  		fmt.Fprintln(os.Stderr, "no torrents specified")
   106  		return
   107  	}
   108  
   109  	tmpdir, err := os.MkdirTemp("", "torrent-pick-")
   110  	if err != nil {
   111  		log.Fatal(err)
   112  	}
   113  
   114  	defer os.RemoveAll(tmpdir)
   115  
   116  	rootGroup.Client.DataDir = tmpdir
   117  
   118  	client, err := torrent.NewClient(rootGroup.Client)
   119  	if err != nil {
   120  		log.Fatalf("error creating client: %s", err)
   121  	}
   122  	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
   123  		client.WriteStatus(w)
   124  	})
   125  	defer client.Close()
   126  
   127  	dstName := dstFileName(rootGroup.Pick)
   128  
   129  	f, err := os.Create(dstName)
   130  	if err != nil {
   131  		log.Fatal(err)
   132  	}
   133  	dstWriter := bufio.NewWriter(f)
   134  
   135  	done := make(chan struct{})
   136  	for _, arg := range posArgs {
   137  		t := func() *torrent.Torrent {
   138  			if strings.HasPrefix(arg, "magnet:") {
   139  				t, err := client.AddMagnet(arg)
   140  				if err != nil {
   141  					log.Fatalf("error adding magnet: %s", err)
   142  				}
   143  				return t
   144  			} else {
   145  				metaInfo, err := metainfo.LoadFromFile(arg)
   146  				if err != nil {
   147  					log.Fatal(err)
   148  				}
   149  				t, err := client.AddTorrent(metaInfo)
   150  				if err != nil {
   151  					log.Fatal(err)
   152  				}
   153  				return t
   154  			}
   155  		}()
   156  		t.AddPeers(testPeers)
   157  
   158  		go func() {
   159  			defer close(done)
   160  			<-t.GotInfo()
   161  			for _, file := range t.Files() {
   162  				if file.DisplayPath() != rootGroup.Pick {
   163  					continue
   164  				}
   165  				file.Download()
   166  				srcReader := file.NewReader()
   167  				defer srcReader.Close()
   168  				io.Copy(dstWriter, srcReader)
   169  				return
   170  			}
   171  			log.Print("file not found")
   172  		}()
   173  	}
   174  
   175  	ticker := time.NewTicker(time.Second)
   176  	defer ticker.Stop()
   177  waitDone:
   178  	for {
   179  		select {
   180  		case <-done:
   181  			break waitDone
   182  		case <-ticker.C:
   183  			os.Stdout.WriteString(progressLine(client))
   184  		}
   185  	}
   186  	if rootGroup.Client.Seed {
   187  		select {}
   188  	}
   189  }