diff --git a/cmd/toru/download.go b/cmd/toru/download.go index 151d5ad..901b6f5 100644 --- a/cmd/toru/download.go +++ b/cmd/toru/download.go @@ -3,42 +3,191 @@ package main import ( "context" "fmt" + "log" + "os" + "time" + "github.com/anacrolix/torrent" "github.com/pterm/pterm" "github.com/sweetbbak/toru/pkg/libtorrent" + "github.com/sweetbbak/toru/pkg/search" ) -func DownloadTorrent(cl *libtorrent.Client) error { - var tfile string +// +// +// +// - if download.TorrentFile != "" { - tfile = download.TorrentFile - } else if download.Args.Query != "" { - tfile = download.Args.Query +func DownloadMain(cl *libtorrent.Client) error { + var outputName string + if download.Directory != "" { + outputName = string(download.Directory) } else { - return fmt.Errorf("download: missing argument (magnet|torrent|url) OR --torrent flag") + outputName = "toru-media" } - success, _ := pterm.DefaultSpinner.Start("getting torrent info") + // create download dir + if err := CreateOutput(outputName); err != nil { + return err + } + + // set download dir lol + cl.SetDownloadDir(outputName) + + // no need to serve torrents + // cl.SetServerOFF(true) + tmp := os.TempDir() + opt := libtorrent.SetDataDir(tmp) + + if err := cl.Init(opt); err != nil { + return err + } - t, err := cl.AddTorrent(tfile) + torrents, err := DownloadMultiple(cl) if err != nil { return err } - success.Success("Success!") + // Create a multi printer for managing multiple printers + multi := pterm.DefaultMultiPrinter + + var pbars []*pterm.ProgressbarPrinter + + for _, t := range torrents { + pb, _ := pterm.DefaultProgressbar.WithTotal(100).WithWriter(multi.NewWriter()).Start(TruncateString(t.Name(), 30)) + pbars = append(pbars, pb) + } + + _, err = multi.Start() + if err != nil { + log.Println(err) + } + ctx, cancel := context.WithCancel(context.Background()) go func() { - Progress(t, ctx) + for { + select { + case <-ctx.Done(): + multi.Stop() + return + default: + for i, t := range torrents { + pb := pbars[i] + pc := float64(t.BytesCompleted()) / float64(t.Length()) * 100 + numpeers := len(t.PeerConns()) + pb.Increment().Current = int(pc) + pb.UpdateTitle(fmt.Sprintf("peers [%02d]", numpeers)) + time.Sleep(time.Millisecond * 5) + } + } + } }() - t.DownloadAll() - if cl.TorrentClient.WaitAll() { - cancel() - return nil + for { + if cl.TorrentClient.WaitAll() { + cancel() + println("done!") + return nil + } + } + +} + +// TODO: create a function that just handles query building +func SearchAnime(q *Search, term string) (*search.Results, error) { + s := search.NewSearch() + + // build the query + if q.Category != "" { + s.Category = q.Category + } + if q.Filter != "" { + s.Filter = q.Filter + } + if q.SortBy != "" { + s.SortBy = q.SortBy + } + if q.SortOrder != "" { + s.SortOrder = q.SortOrder + } + if q.User != "" { + s.User = q.User + } + if q.Args.Query != "" { + s.Args.Query = q.Args.Query + } + if term != "" { + s.Args.Query = q.Args.Query + } + if q.Page != 0 { + s.Page = q.Page + } + + if q.Latest { + s = &search.Search{ + SortBy: "id", + SortOrder: "desc", + Page: 1, + Category: "subs", + } + } + + if options.Proxy != "" { + s.ProxyURL = options.Proxy + } + + // make the request for results to nyaa.si + m, err := s.Query() + if err != nil { + return nil, err + } + + return m, nil +} + +func DownloadMultiple(cl *libtorrent.Client) ([]*torrent.Torrent, error) { + q := &Search{ + SortBy: download.SortBy, + SortOrder: download.SortOrder, + User: download.User, + Filter: download.Filter, + Page: download.Page, + Latest: download.Latest, + Category: download.Category, + } + + q.Args.Query = download.Query + + m, err := SearchAnime(q, download.Query) + if err != nil { + return nil, err + } + + choices, err := fzfMenuMulti(m.Media) + if err != nil { + return nil, err + } + + var torrents []*torrent.Torrent + for _, item := range choices { + t, err := cl.AddTorrent(item.Magnet) + if err != nil { + log.Println(err) + } + + t.DownloadAll() + torrents = append(torrents, t) + } + + return torrents, nil +} + +func CreateOutput(dir string) error { + _, err := os.Stat(dir) + if err == nil { + return err } else { - cancel() - return fmt.Errorf("Unable to completely download torrent: %s", t.Name()) + return os.MkdirAll(dir, 0o755) } } diff --git a/cmd/toru/flags.go b/cmd/toru/flags.go index c34c8c0..86d7ab4 100644 --- a/cmd/toru/flags.go +++ b/cmd/toru/flags.go @@ -27,11 +27,11 @@ type Completions struct { // Streaming options type Stream struct { - Magnet string `short:"m" long:"magnet" description:"stream directly from the provided torrent magnet link"` - TorrentFile string `short:"t" long:"torrent" description:"stream directly from the provided torrent file or torrent URL"` - Remove bool ` long:"rm" description:"remove cached files after exiting"` - Latest bool `short:"l" long:"latest" description:"view the latest anime and select an episode"` - FromJson flags.Filename `short:"j" long:"from-json" description:"resume selection from prior search saved as json [see: toru search --help]"` + Magnet string `short:"m" long:"magnet" description:"stream directly from the provided torrent magnet link"` + TorrentFile string `short:"t" long:"torrent" description:"stream directly from the provided torrent file or torrent URL"` + Remove bool `long:"rm" description:"remove cached files after exiting"` + Latest bool `short:"l" long:"latest" description:"view the latest anime and select an episode"` + FromJson flags.Filename `short:"j" long:"from-json" description:"resume selection from prior search saved as json [see: toru search --help]"` // optional magnet link or torrent file as a trailing argument instead of explicitly defined Args struct { @@ -39,15 +39,35 @@ type Stream struct { } `positional-args:"yes" positional-arg-name:"TORRENT"` } -// Downloading options +// Downloading options by selecting items from a query type Download struct { - Directory string `short:"d" long:"dir" description:"parent directory to download torrents to"` - TorrentFile string `short:"t" long:"torrent" description:"explicitly define torrent magnet|file|url to download"` + Directory string `short:"o" long:"output" description:"parent directory to download torrents to"` + Query string `short:"q" long:"query" description:"query to search for"` + SortBy string `short:"b" long:"sort-by" description:"sort results by a category [size|date|seeders|leechers|downloads]"` + SortOrder string `short:"O" long:"sort-order" description:"sort by ascending or descending: options [asc|desc]" choice:"asc"` + User string `short:"u" long:"user" description:"search for content by a user"` + Filter string `short:"f" long:"filter" description:"filter content. Options: [no-remakes|trusted]"` + Page uint `short:"p" long:"page" description:"which results page to display [default 1]"` + Stream bool `short:"s" long:"stream" description:"stream selected torrents after search"` + Multi bool `short:"m" long:"multi" description:"choose multiple torrents to queue for downloading or streaming"` + Latest bool `short:"n" long:"latest" description:"view the latest anime"` + Category string `short:"c" long:"category" description:"search torrents by a category: run [toru search --list] to see categories"` // magnet link, torrent or torrent file url Args struct { Query string - } `positional-args:"yes" positional-arg-name:"TORRENT"` + } `positional-args:"yes" positional-arg-name:"QUERY"` +} + +// Downloading options, moved to simple download section +type WGET struct { + Directory flags.Filename `short:"d" long:"dir" description:"parent directory to download torrents to"` + TorrentFile flags.Filename `short:"t" long:"torrent" description:"explicitly define torrent magnet|file|url to download"` + + // magnet link, torrent or torrent file url + Args struct { + Query string + } `positional-args:"yes" positional-arg-name:"QUERY"` } type Latest struct{} @@ -55,7 +75,7 @@ type Latest struct{} // Non-interactive CLI search options type Search struct { SortBy string `short:"b" long:"sort-by" description:"sort results by a category [size|date|seeders|leechers|downloads]"` - SortOrder string `short:"o" long:"sort-order" description:"sort by ascending or descending: options [asc|desc]" choice:"asc"` + SortOrder string `short:"o" long:"sort-order" description:"sort by ascending or descending: options [asc|desc]" choice:"asc"` User string `short:"u" long:"user" description:"search for content by a user"` Filter string `short:"f" long:"filter" description:"filter content. Options: [no-remakes|trusted]"` Page uint `short:"p" long:"page" description:"which results page to display [default 1]"` @@ -74,4 +94,3 @@ type Search struct { Query string } `positional-args:"yes" positional-arg-name:"QUERY"` } - diff --git a/cmd/toru/main.go b/cmd/toru/main.go index 1df03f7..a2b5fd0 100644 --- a/cmd/toru/main.go +++ b/cmd/toru/main.go @@ -22,6 +22,7 @@ var ( searchopts Search download Download completions Completions + wget WGET latest Latest ) @@ -41,11 +42,15 @@ func init() { if err != nil { log.Fatal(err) } - d, err := parser.AddCommand("download", "download torrents", "download torrent from .torrent file, magnet or URL to a .torrent file", &download) + d, err := parser.AddCommand("download", "select one or many torrents to download", "download torrent from .torrent file, magnet or URL to a .torrent file", &download) if err != nil { log.Fatal(err) } + _, err = parser.AddCommand("wget", "wget a torrent file", "wget a torrent file", &wget) + if err != nil { + log.Fatal(err) + } _, err = parser.AddCommand("latest", "get the latest anime", "get the latest anime from nyaa.si", &latest) if err != nil { log.Fatal(err) @@ -108,6 +113,13 @@ func main() { cl.TorrentPort = options.TorrentPort } + if parser.Active.Name == "download" { + if err := DownloadMain(cl); err != nil { + log.Fatal(err) + } + os.Exit(0) + } + if err := cl.Init(); err != nil { log.Fatal(err) } @@ -121,7 +133,7 @@ func main() { if err := runStream(cl); err != nil { log.Fatal(err) } - case "dl", "download": + case "wget": if err := DownloadTorrent(cl); err != nil { log.Fatal(err) } diff --git a/cmd/toru/search.go b/cmd/toru/search.go index 79fdd89..af34029 100644 --- a/cmd/toru/search.go +++ b/cmd/toru/search.go @@ -254,3 +254,36 @@ func fzfMenu(m []nyaa.Media) (nyaa.Media, error) { return m[idx], nil } + +func fzfMenuMulti(m []nyaa.Media) ([]nyaa.Media, error) { + idxs, err := fzf.FindMulti( + m, + func(i int) string { + return m[i].Name + }, + fzf.WithPreviewWindow(func(i, width, height int) string { + if i == -1 { + return "lol" + } + + return FormatMedia(m[i]) + + }), + ) + + var matches []nyaa.Media + for _, item := range idxs { + matches = append(matches, m[item]) + } + + // User has selected nothing + if err != nil { + if errors.Is(err, fzf.ErrAbort) { + os.Exit(0) + } else { + return nil, err + } + } + + return matches, nil +} diff --git a/cmd/toru/wget.go b/cmd/toru/wget.go new file mode 100644 index 0000000..6e52773 --- /dev/null +++ b/cmd/toru/wget.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + "github.com/pterm/pterm" + "github.com/sweetbbak/toru/pkg/libtorrent" +) + +func DownloadTorrent(cl *libtorrent.Client) error { + var tfile string + + if wget.TorrentFile != "" { + tfile = string(wget.TorrentFile) + } else if download.Args.Query != "" { + tfile = download.Args.Query + } else { + return fmt.Errorf("download: missing argument (magnet|torrent|url) OR --torrent flag") + } + + success, _ := pterm.DefaultSpinner.Start("getting torrent info") + + t, err := cl.AddTorrent(tfile) + if err != nil { + return err + } + + success.Success("Success!") + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + Progress(t, ctx) + }() + + t.DownloadAll() + if cl.TorrentClient.WaitAll() { + cancel() + return nil + } else { + cancel() + return fmt.Errorf("Unable to completely download torrent: %s", t.Name()) + } +} diff --git a/pkg/libtorrent/client.go b/pkg/libtorrent/client.go index 3dee1cb..61a02a9 100644 --- a/pkg/libtorrent/client.go +++ b/pkg/libtorrent/client.go @@ -22,10 +22,14 @@ const ( type Client struct { // client / project name, will be the default directory name Name string - // directory to download torrents to + // directory to hold streamed torrents and metainfo DataDir string + // directory to download to based on user defined input + DownloadDir string // Seed or no Seed bool + // turn off the HTTP server for streaming torrents + NoServer bool // Port to stream torrents on Port string // Port to stream torrents on @@ -43,14 +47,34 @@ type Client struct { // create a default client, must call Init afterwords func NewClient(name string, port string) *Client { return &Client{ - Name: name, - Port: port, - Seed: false, + Name: name, + Port: port, + NoServer: false, + Seed: true, + } +} + +// set the download location for the torrent client +func (c *Client) SetDownloadDir(dir string) { + c.DownloadDir = dir +} + +// turn off the internal http server that handles streaming if it is not needed +func (c *Client) SetServerOFF(off bool) { + c.NoServer = off +} + +type ClientOption func(args *Client) error + +func SetDataDir(dir string) ClientOption { + return func(args *Client) error { + args.DataDir = dir + return nil } } // Initialize torrent configuration -func (c *Client) Init() error { +func (c *Client) Init(opts ...ClientOption) error { cfg := torrent.NewDefaultClientConfig() s, err := c.getStorage() if err != nil { @@ -78,20 +102,60 @@ func (c *Client) Init() error { } } + // set defaults cfg.ListenPort = c.TorrentPort c.DataDir = s - cfg.DefaultStorage = storage.NewFileByInfoHash(c.DataDir) + + // default overrides + for _, setter := range opts { + if setter == nil { + log.Println("option supplied is nil") + continue + } + + err := setter(c) + if err != nil { + return err + } + } + + var stor storage.ClientImpl + if c.DownloadDir == "" { + c.DownloadDir = s + stor = storage.NewFileByInfoHash(c.DataDir) + } else { + stor, err = getMetadataDir(c.DataDir, c.DownloadDir) + } + + cfg.DefaultStorage = stor client, err := torrent.NewClient(cfg) if err != nil { return fmt.Errorf("error creating a new torrent client: %v", err) } - c.StartServer() + if !c.NoServer { + c.StartServer() + } c.TorrentClient = client return nil } +func getMetadataDir(metadataDir, downloadDir string) (storage.ClientImpl, error) { + mstor, err := storage.NewDefaultPieceCompletionForDir(metadataDir) + if err != nil { + log.Println("unable to set download dir, falling back to data dir:", err) + return storage.NewMMap(downloadDir), nil + } + + tstor := storage.NewMMapWithCompletion(downloadDir, mstor) + if err != nil { + return nil, err + } + + return tstor, err +} + // add a torrent and mark it entirely for download func (c *Client) DownloadTorrent(torrent string) error { t, err := c.AddTorrent(torrent)