1 // Downloads torrents from the command-line.
20 "github.com/anacrolix/args"
21 "github.com/anacrolix/envpprof"
22 "github.com/anacrolix/log"
23 "github.com/anacrolix/missinggo/v2"
24 "github.com/anacrolix/tagflag"
25 "github.com/anacrolix/torrent/bencode"
26 pp "github.com/anacrolix/torrent/peer_protocol"
27 "github.com/anacrolix/torrent/version"
28 "github.com/davecgh/go-spew/spew"
29 "github.com/dustin/go-humanize"
30 "golang.org/x/time/rate"
32 "github.com/anacrolix/torrent"
33 "github.com/anacrolix/torrent/iplist"
34 "github.com/anacrolix/torrent/metainfo"
35 "github.com/anacrolix/torrent/storage"
38 func torrentBar(t *torrent.Torrent, pieceStates bool) {
42 fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
45 lastStats := t.Stats()
47 interval := 3 * time.Second
48 for range time.Tick(interval) {
49 var completedPieces, partialPieces int
50 psrs := t.PieceStateRuns()
51 for _, r := range psrs {
53 completedPieces += r.Length
56 partialPieces += r.Length
60 byteRate := int64(time.Second)
61 byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
62 byteRate /= int64(interval)
64 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
67 humanize.Bytes(uint64(t.BytesCompleted())),
68 humanize.Bytes(uint64(t.Length())),
72 humanize.Bytes(uint64(byteRate)),
76 os.Stdout.WriteString(line)
86 type stringAddr string
88 func (stringAddr) Network() string { return "" }
89 func (me stringAddr) String() string { return string(me) }
91 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
92 for _, ta := range addrs {
93 ret = append(ret, torrent.PeerInfo{
100 func addTorrents(client *torrent.Client, flags downloadFlags) error {
101 testPeers := resolveTestPeers(flags.TestPeer)
102 for _, arg := range flags.Torrent {
103 t, err := func() (*torrent.Torrent, error) {
104 if strings.HasPrefix(arg, "magnet:") {
105 t, err := client.AddMagnet(arg)
107 return nil, fmt.Errorf("error adding magnet: %w", err)
110 } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
111 response, err := http.Get(arg)
113 return nil, fmt.Errorf("Error downloading torrent file: %s", err)
116 metaInfo, err := metainfo.Load(response.Body)
117 defer response.Body.Close()
119 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
121 t, err := client.AddTorrent(metaInfo)
123 return nil, fmt.Errorf("adding torrent: %w", err)
126 } else if strings.HasPrefix(arg, "infohash:") {
127 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
130 metaInfo, err := metainfo.LoadFromFile(arg)
132 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
134 t, err := client.AddTorrent(metaInfo)
136 return nil, fmt.Errorf("adding torrent: %w", err)
142 return fmt.Errorf("adding torrent for %q: %w", arg, err)
145 torrentBar(t, flags.PieceStates)
147 t.AddPeers(testPeers)
150 if len(flags.File) == 0 {
153 for _, f := range t.Files() {
154 for _, fileArg := range flags.File {
155 if f.DisplayPath() == fileArg {
166 type downloadFlags struct {
171 type DownloadCmd struct {
172 Mmap bool `help:"memory-map torrent data"`
173 TestPeer []string `help:"addresses of some starting peers"`
174 Seed bool `help:"seed after download is complete"`
175 Addr string `help:"network listen addr"`
176 MaxUnverifiedBytes tagflag.Bytes `help:"maximum number bytes to have pending verification"`
177 UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"`
178 DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"`
179 PackedBlocklist string
181 Progress bool `default:"true"`
183 Quiet bool `help:"discard client logging"`
184 Stats bool `help:"print stats at termination"`
185 Dht bool `default:"true"`
187 TcpPeers bool `default:"true"`
188 UtpPeers bool `default:"true"`
189 Webtorrent bool `default:"true"`
191 // Don't progress past handshake for peer connections where the peer doesn't offer the fast
193 RequireFastExtension bool
195 Ipv4 bool `default:"true"`
196 Ipv6 bool `default:"true"`
197 Pex bool `default:"true"`
200 Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
203 func statsEnabled(flags downloadFlags) bool {
207 func exitSignalHandlers(notify *missinggo.SynchronizedEvent) {
208 c := make(chan os.Signal, 1)
209 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
211 log.Printf("close signal received: %+v", <-c)
217 if err := mainErr(); err != nil {
218 log.Printf("error in main: %v", err)
223 func mainErr() error {
224 defer envpprof.Stop()
225 stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile)
226 debug := args.Flag(args.FlagOpt{Long: "debug"})
229 args.Subcommand("metainfo", metainfoCmd),
230 args.Subcommand("announce", func(p args.SubCmdCtx) error {
232 err := p.NewParser().AddParams(
233 args.Pos("tracker", &cmd.Tracker),
234 args.Pos("infohash", &cmd.InfoHash)).Parse()
238 return announceErr(cmd)
240 args.Subcommand("download", func(p args.SubCmdCtx) error {
242 err := p.NewParser().AddParams(
243 append(args.FromStruct(&dlc), debug)...,
248 dlf := downloadFlags{
252 p.Defer(func() error {
253 return downloadErr(dlf)
259 func(p args.SubCmdCtx) error {
260 var print func(interface{}) error
262 args.Subcommand("json", func(ctx args.SubCmdCtx) (err error) {
264 je := json.NewEncoder(os.Stdout)
265 je.SetIndent("", " ")
269 args.Subcommand("spew", func(ctx args.SubCmdCtx) (err error) {
271 config := spew.NewDefaultConfig()
272 config.DisableCapacities = true
274 print = func(v interface{}) error {
281 return errors.New("an output type is required")
283 d := bencode.NewDecoder(os.Stdin)
284 p.Defer(func() error {
292 return fmt.Errorf("decoding message index %d: %w", i, err)
300 args.Help("reads bencoding from stdin into Go native types and spews the result"),
302 args.Subcommand("version", func(p args.SubCmdCtx) error {
303 fmt.Printf("HTTP User-Agent: %q\n", version.DefaultHttpUserAgent)
304 fmt.Printf("Torrent client version: %q\n", version.DefaultExtendedHandshakeClientVersion)
305 fmt.Printf("Torrent version prefix: %q\n", version.DefaultBep20Prefix)
312 func downloadErr(flags downloadFlags) error {
313 clientConfig := torrent.NewDefaultClientConfig()
314 clientConfig.DisableWebseeds = flags.DisableWebseeds
315 clientConfig.DisableTCP = !flags.TcpPeers
316 clientConfig.DisableUTP = !flags.UtpPeers
317 clientConfig.DisableIPv4 = !flags.Ipv4
318 clientConfig.DisableIPv6 = !flags.Ipv6
319 clientConfig.DisableAcceptRateLimiting = true
320 clientConfig.NoDHT = !flags.Dht
321 clientConfig.Debug = flags.Debug
322 clientConfig.Seed = flags.Seed
323 clientConfig.PublicIp4 = flags.PublicIP
324 clientConfig.PublicIp6 = flags.PublicIP
325 clientConfig.DisablePEX = !flags.Pex
326 clientConfig.DisableWebtorrent = !flags.Webtorrent
327 if flags.PackedBlocklist != "" {
328 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
330 return fmt.Errorf("loading blocklist: %v", err)
332 defer blocklist.Close()
333 clientConfig.IPBlocklist = blocklist
336 clientConfig.DefaultStorage = storage.NewMMap("")
338 if flags.Addr != "" {
339 clientConfig.SetListenAddr(flags.Addr)
341 if flags.UploadRate != nil {
342 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
344 if flags.DownloadRate != nil {
345 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<20)
348 clientConfig.Logger = log.Discard
350 if flags.RequireFastExtension {
351 clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
353 clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
355 var stop missinggo.SynchronizedEvent
360 client, err := torrent.NewClient(clientConfig)
362 return fmt.Errorf("creating client: %w", err)
364 var clientClose sync.Once //In certain situations, close was being called more than once.
365 defer clientClose.Do(func() { client.Close() })
366 go exitSignalHandlers(&stop)
369 clientClose.Do(func() { client.Close() })
372 // Write status on the root path on the default HTTP muxer. This will be bound to localhost
373 // somewhere if GOPPROF is set, thanks to the envpprof import.
374 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
375 client.WriteStatus(w)
377 err = addTorrents(client, flags)
378 started := time.Now()
380 return fmt.Errorf("adding torrents: %w", err)
382 defer outputStats(client, flags)
383 if client.WaitAll() {
384 log.Print("downloaded ALL the torrents")
386 err = errors.New("y u no complete torrents?!")
388 clientConnStats := client.ConnStats()
389 log.Printf("average download rate: %v",
390 humanize.Bytes(uint64(
392 clientConnStats.BytesReadUsefulData.Int64(),
393 )*time.Second/time.Since(started),
396 if len(client.Torrents()) == 0 {
397 log.Print("no torrents to seed")
399 outputStats(client, flags)
403 spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received"))
404 spew.Dump(client.ConnStats())
405 clStats := client.ConnStats()
406 sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
408 "client read %v, %.1f%% was useful data. sent %v non-data bytes",
409 humanize.Bytes(uint64(clStats.BytesRead.Int64())),
410 100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
411 humanize.Bytes(uint64(sentOverhead)))
415 func outputStats(cl *torrent.Client, args downloadFlags) {
416 if !statsEnabled(args) {
419 expvar.Do(func(kv expvar.KeyValue) {
420 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
422 cl.WriteStatus(os.Stdout)