1 // Downloads torrents from the command-line.
19 "github.com/anacrolix/args"
20 "github.com/anacrolix/envpprof"
21 "github.com/anacrolix/log"
22 "github.com/anacrolix/missinggo/v2"
23 "github.com/anacrolix/tagflag"
24 "github.com/anacrolix/torrent/bencode"
25 "github.com/anacrolix/torrent/version"
26 "github.com/davecgh/go-spew/spew"
27 "github.com/dustin/go-humanize"
28 "golang.org/x/time/rate"
30 "github.com/anacrolix/torrent"
31 "github.com/anacrolix/torrent/iplist"
32 "github.com/anacrolix/torrent/metainfo"
33 "github.com/anacrolix/torrent/storage"
36 func torrentBar(t *torrent.Torrent, pieceStates bool) {
40 fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
43 lastStats := t.Stats()
45 interval := 3 * time.Second
46 for range time.Tick(interval) {
47 var completedPieces, partialPieces int
48 psrs := t.PieceStateRuns()
49 for _, r := range psrs {
51 completedPieces += r.Length
54 partialPieces += r.Length
58 byteRate := int64(time.Second)
59 byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
60 byteRate /= int64(interval)
62 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
65 humanize.Bytes(uint64(t.BytesCompleted())),
66 humanize.Bytes(uint64(t.Length())),
70 humanize.Bytes(uint64(byteRate)),
74 os.Stdout.WriteString(line)
84 type stringAddr string
86 func (stringAddr) Network() string { return "" }
87 func (me stringAddr) String() string { return string(me) }
89 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
90 for _, ta := range addrs {
91 ret = append(ret, torrent.PeerInfo{
98 func addTorrents(client *torrent.Client, flags downloadFlags) error {
99 testPeers := resolveTestPeers(flags.TestPeer)
100 for _, arg := range flags.Torrent {
101 t, err := func() (*torrent.Torrent, error) {
102 if strings.HasPrefix(arg, "magnet:") {
103 t, err := client.AddMagnet(arg)
105 return nil, fmt.Errorf("error adding magnet: %w", err)
108 } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
109 response, err := http.Get(arg)
111 return nil, fmt.Errorf("Error downloading torrent file: %s", err)
114 metaInfo, err := metainfo.Load(response.Body)
115 defer response.Body.Close()
117 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
119 t, err := client.AddTorrent(metaInfo)
121 return nil, fmt.Errorf("adding torrent: %w", err)
124 } else if strings.HasPrefix(arg, "infohash:") {
125 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
128 metaInfo, err := metainfo.LoadFromFile(arg)
130 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
132 t, err := client.AddTorrent(metaInfo)
134 return nil, fmt.Errorf("adding torrent: %w", err)
140 return fmt.Errorf("adding torrent for %q: %w", arg, err)
143 torrentBar(t, flags.PieceStates)
145 t.AddPeers(testPeers)
148 if len(flags.File) == 0 {
151 for _, f := range t.Files() {
152 for _, fileArg := range flags.File {
153 if f.DisplayPath() == fileArg {
164 type downloadFlags struct {
169 type DownloadCmd struct {
170 Mmap bool `help:"memory-map torrent data"`
171 TestPeer []string `help:"addresses of some starting peers"`
172 Seed bool `help:"seed after download is complete"`
173 Addr string `help:"network listen addr"`
174 MaxUnverifiedBytes tagflag.Bytes `help:"maximum number bytes to have pending verification"`
175 UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"`
176 DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"`
177 PackedBlocklist string
179 Progress bool `default:"true"`
181 Quiet bool `help:"discard client logging"`
182 Stats bool `help:"print stats at termination"`
183 Dht bool `default:"true"`
185 TcpPeers bool `default:"true"`
186 UtpPeers bool `default:"true"`
187 Webtorrent bool `default:"true"`
190 Ipv4 bool `default:"true"`
191 Ipv6 bool `default:"true"`
192 Pex bool `default:"true"`
195 Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
198 func statsEnabled(flags downloadFlags) bool {
202 func exitSignalHandlers(notify *missinggo.SynchronizedEvent) {
203 c := make(chan os.Signal, 1)
204 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
206 log.Printf("close signal received: %+v", <-c)
212 if err := mainErr(); err != nil {
213 log.Printf("error in main: %v", err)
218 func mainErr() error {
219 defer envpprof.Stop()
220 stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile)
221 debug := args.Flag(args.FlagOpt{Long: "debug"})
224 args.Subcommand("metainfo", metainfoCmd),
225 args.Subcommand("announce", func(p args.SubCmdCtx) error {
227 err := p.NewParser().AddParams(
228 args.Pos("tracker", &cmd.Tracker),
229 args.Pos("infohash", &cmd.InfoHash)).Parse()
233 return announceErr(cmd)
235 args.Subcommand("download", func(p args.SubCmdCtx) error {
237 err := p.NewParser().AddParams(
238 append(args.FromStruct(&dlf), debug)...,
243 return downloadErr(downloadFlags{
250 func(p args.SubCmdCtx) error {
251 d := bencode.NewDecoder(os.Stdin)
259 return fmt.Errorf("decoding message index %d: %w", i, err)
265 args.Help("reads bencoding from stdin into Go native types and spews the result"),
267 args.Subcommand("version", func(p args.SubCmdCtx) error {
268 fmt.Printf("HTTP User-Agent: %q\n", version.DefaultHttpUserAgent)
269 fmt.Printf("Torrent client version: %q\n", version.DefaultExtendedHandshakeClientVersion)
270 fmt.Printf("Torrent version prefix: %q\n", version.DefaultBep20Prefix)
275 if errors.Is(p.Err, args.ErrHelped) {
281 p.PrintChoices(os.Stderr)
287 func downloadErr(flags downloadFlags) error {
288 clientConfig := torrent.NewDefaultClientConfig()
289 clientConfig.DisableWebseeds = flags.DisableWebseeds
290 clientConfig.DisableTCP = !flags.TcpPeers
291 clientConfig.DisableUTP = !flags.UtpPeers
292 clientConfig.DisableIPv4 = !flags.Ipv4
293 clientConfig.DisableIPv6 = !flags.Ipv6
294 clientConfig.DisableAcceptRateLimiting = true
295 clientConfig.NoDHT = !flags.Dht
296 clientConfig.Debug = flags.Debug
297 clientConfig.Seed = flags.Seed
298 clientConfig.PublicIp4 = flags.PublicIP
299 clientConfig.PublicIp6 = flags.PublicIP
300 clientConfig.DisablePEX = !flags.Pex
301 clientConfig.DisableWebtorrent = !flags.Webtorrent
302 if flags.PackedBlocklist != "" {
303 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
305 return fmt.Errorf("loading blocklist: %v", err)
307 defer blocklist.Close()
308 clientConfig.IPBlocklist = blocklist
311 clientConfig.DefaultStorage = storage.NewMMap("")
313 if flags.Addr != "" {
314 clientConfig.SetListenAddr(flags.Addr)
316 if flags.UploadRate != nil {
317 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
319 if flags.DownloadRate != nil {
320 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<20)
323 clientConfig.Logger = log.Discard
325 clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
327 var stop missinggo.SynchronizedEvent
332 client, err := torrent.NewClient(clientConfig)
334 return fmt.Errorf("creating client: %w", err)
336 var clientClose sync.Once //In certain situations, close was being called more than once.
337 defer clientClose.Do(func() { client.Close() })
338 go exitSignalHandlers(&stop)
341 clientClose.Do(func() { client.Close() })
344 // Write status on the root path on the default HTTP muxer. This will be bound to localhost
345 // somewhere if GOPPROF is set, thanks to the envpprof import.
346 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
347 client.WriteStatus(w)
349 err = addTorrents(client, flags)
350 started := time.Now()
352 return fmt.Errorf("adding torrents: %w", err)
354 defer outputStats(client, flags)
355 if client.WaitAll() {
356 log.Print("downloaded ALL the torrents")
358 err = errors.New("y u no complete torrents?!")
360 clientConnStats := client.ConnStats()
361 log.Printf("average download rate: %v",
362 humanize.Bytes(uint64(
364 clientConnStats.BytesReadUsefulData.Int64(),
365 )*time.Second/time.Since(started),
368 if len(client.Torrents()) == 0 {
369 log.Print("no torrents to seed")
371 outputStats(client, flags)
375 spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received"))
376 spew.Dump(client.ConnStats())
377 clStats := client.ConnStats()
378 sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
380 "client read %v, %.1f%% was useful data. sent %v non-data bytes",
381 humanize.Bytes(uint64(clStats.BytesRead.Int64())),
382 100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
383 humanize.Bytes(uint64(sentOverhead)))
387 func outputStats(cl *torrent.Client, args downloadFlags) {
388 if !statsEnabled(args) {
391 expvar.Do(func(kv expvar.KeyValue) {
392 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
394 cl.WriteStatus(os.Stdout)