Skip to content

Commit

Permalink
add export cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
sagan committed Mar 7, 2024
1 parent d67a630 commit f604364
Show file tree
Hide file tree
Showing 28 changed files with 259 additions and 86 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ ptool <command> args... [flags]
- search : 在某个站点搜索指定关键词的种子。
- add : 将种子添加到 BT 客户端。
- dltorrent : 下载站点的种子。
- BT 客户端控制命令集: clientctl / show / pause / resume / delete / reannounce / recheck / getcategories / createcategory / deletecategories / setcategory / gettags / createtags / deletetags / addtags / removetags / renametag / edittracker / addtrackers / removetrackers / setsavepath。
- BT 客户端控制命令集: clientctl / show / pause / resume / delete / reannounce / recheck / getcategories / createcategory / deletecategories / setcategory / gettags / createtags / deletetags / addtags / removetags / renametag / edittracker / addtrackers / removetrackers / setsavepath / export
- parsetorrent : 显示种子(torrent)文件信息。
- verifytorrent : 测试种子(torrent)文件与硬盘上的文件内容一致。
- partialdownload : 拆包下载。
Expand Down Expand Up @@ -336,6 +336,14 @@ ptool removetrackers <client> <infoHashes...> --tracker "https://..."
ptool setsavepath <client> <savePath> [<infoHash>...]
```

#### 导出客户端种子 (export)

```
ptool export <client> <infoHash>...
```

导出客户端里的种子为 .torrent 文件。

### 显示 BT 客户端或 PT 站点状态 (status)

```
Expand Down
2 changes: 2 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ type TorrentCategory struct {
}

type Client interface {
// download / export .torrent file for a torrent in client
ExportTorrentFile(infoHash string) ([]byte, error)
GetTorrent(infoHash string) (*Torrent, error)
// stateFilter: _all|_active|_done|_undone, or any state value (possibly with a _ prefix)
GetTorrents(stateFilter string, category string, showAll bool) ([]Torrent, error)
Expand Down
13 changes: 13 additions & 0 deletions client/qbittorrent/qbittorrent.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,19 @@ func (qbclient *Client) SetConfig(variable string, value string) error {
}
}

// The export API of qb exists but currently is not documented in
// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1) .
// See https://github.com/qbittorrent/qBittorrent/issues/18746 for more info.
func (qbclient *Client) ExportTorrentFile(infoHash string) ([]byte, error) {
apiUrl := qbclient.ClientConfig.Url + "api/v2/torrents/export?hash=" + infoHash
res, _, err := util.FetchUrl(apiUrl, qbclient.HttpClient)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}

func (qbclient *Client) GetTorrent(infoHash string) (*client.Torrent, error) {
err := qbclient.sync()
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions client/transmission/transmission.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ func (trclient *Client) syncMeta() error {
return nil
}

func (trclient *Client) ExportTorrentFile(infoHash string) ([]byte, error) {
return nil, fmt.Errorf("unsupported")
}

func (trclient *Client) GetTorrent(infoHash string) (*client.Torrent, error) {
if err := trclient.sync(); err != nil {
return nil, err
Expand Down
4 changes: 3 additions & 1 deletion cmd/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ as a special case, it also supports directly reading .torrent file contents from
* [id] : Torrent id in site
* [site] : Torrent site
* [filename] : Original torrent filename without ".torrent" extension
* [name] : Torrent name`,
* [filename128] : The prefix of [filename] which is at max 128 bytes
* [name] : Torrent name
* [name128] : The prefix of torrent name which is at max 128 bytes`,
Args: cobra.MatchAll(cobra.MinimumNArgs(2), cobra.OnlyValidArgs),
RunE: add,
}
Expand Down
7 changes: 2 additions & 5 deletions cmd/addtags/addtags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
)

var command = &cobra.Command{
Expand Down Expand Up @@ -47,10 +47,7 @@ func addtags(cmd *cobra.Command, args []string) error {
return fmt.Errorf("you must provide at least a condition flag or hashFilter")
}
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if config.InShell {
return fmt.Errorf(`"-" arg can not be used in shell`)
}
if data, err := util.ReadArgsFromStdin(); err != nil {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
Expand Down
7 changes: 2 additions & 5 deletions cmd/addtrackers/addtrackers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
)

var command = &cobra.Command{
Expand Down Expand Up @@ -59,10 +59,7 @@ func addtrackers(cmd *cobra.Command, args []string) error {
return fmt.Errorf("you must provide at least a condition flag or hashFilter")
}
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if config.InShell {
return fmt.Errorf(`"-" arg can not be used in shell`)
}
if data, err := util.ReadArgsFromStdin(); err != nil {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
Expand Down
1 change: 1 addition & 0 deletions cmd/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
_ "github.com/sagan/ptool/cmd/deletetags"
_ "github.com/sagan/ptool/cmd/dltorrent"
_ "github.com/sagan/ptool/cmd/edittracker"
_ "github.com/sagan/ptool/cmd/export"
_ "github.com/sagan/ptool/cmd/getcategories"
_ "github.com/sagan/ptool/cmd/gettags"
_ "github.com/sagan/ptool/cmd/iyuu/all"
Expand Down
4 changes: 3 additions & 1 deletion cmd/batchdl/batchdl.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ var command = &cobra.Command{
* [id] : Torrent id in site
* [site] : Torrent site
* [filename] : Original torrent filename without ".torrent" extension
* [name] : Torrent name`,
* [filename128] : The prefix of [filename] which is at max 128 bytes
* [name] : Torrent name
* [name128] : The prefix of torrent name which is at max 128 bytes`,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: batchdl,
}
Expand Down
16 changes: 13 additions & 3 deletions cmd/clientctl/clientctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,21 @@ type Option struct {
}

var command = &cobra.Command{
Use: "clientctl {client} [<variable>[=value] ...]",
Use: "clientctl {client} [{variable}[={value}] ...]",
Annotations: map[string]string{"cobra-prompt-dynamic-suggestions": "clientctl"},
Short: "Get or set client config.",
Long: `Get or set client config.`,
RunE: clientctl,
Long: `Get or set client config.
If '[={value}]' part is present, set the config, otherwise get current config.
{variable}: snake_case style config key. e.g.: global_download_speed_limit
{value}: the value of config item to set. For config item of boolean type, use literal "false" or "true";
for config item of size or speed type, use unit chars (B/K/M/G/T/P/E), e.g.: "10M" means 10MiB or 10MiB/s.
Example:
ptool clientctl local save_path # display current default download dir
ptool clientctl local global_upload_speed_limit=10M # set global upload speed limit of local to 10MiB/s
For list of all supported variables, run 'ptool clientctl --parameters'`,
RunE: clientctl,
}

var (
Expand Down
7 changes: 2 additions & 5 deletions cmd/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
)

var command = &cobra.Command{
Expand Down Expand Up @@ -69,10 +69,7 @@ func delete(cmd *cobra.Command, args []string) error {
} else {
// special case. read info hashes from stdin
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if config.InShell {
return fmt.Errorf(`"-" arg can not be used in shell`)
}
if data, err := util.ReadArgsFromStdin(); err != nil {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
Expand Down
6 changes: 4 additions & 2 deletions cmd/dltorrent/dltorrent.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ Torrent url that does NOT belong to any site (e.g.: a public site url) is also s
* [id] : Torrent id in site
* [site] : Torrent site
* [filename] : Original torrent filename without ".torrent" extension
* [name] : Torrent name`,
* [filename128] : The prefix of [filename] which is at max 128 bytes
* [name] : Torrent name
* [name128] : The prefix of torrent name which is at max 128 bytes`,
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.OnlyValidArgs),
RunE: dltorrent,
}
Expand All @@ -38,7 +40,7 @@ var (

func init() {
command.Flags().StringVarP(&defaultSite, "site", "", "", "Set default site of torrents")
command.Flags().StringVarP(&downloadDir, "dir", "", ".", `Set the dir of downloaded torrents`)
command.Flags().StringVarP(&downloadDir, "download-dir", "", ".", `Set the dir of downloaded torrents`)
command.Flags().StringVarP(&rename, "rename", "", "", "Rename downloaded torrents (supports variables)")
cmd.RootCmd.AddCommand(command)
}
Expand Down
7 changes: 2 additions & 5 deletions cmd/edittracker/edittracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
)

var command = &cobra.Command{
Expand Down Expand Up @@ -71,10 +71,7 @@ func edittracker(cmd *cobra.Command, args []string) error {
return fmt.Errorf("you must provide at least a condition flag or hashFilter")
}
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if config.InShell {
return fmt.Errorf(`"-" arg can not be used in shell`)
}
if data, err := util.ReadArgsFromStdin(); err != nil {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
Expand Down
105 changes: 105 additions & 0 deletions cmd/export/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package export

import (
"fmt"
"os"
"path"

"github.com/spf13/cobra"

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/util/helper"
"github.com/sagan/ptool/util/torrentutil"
)

var command = &cobra.Command{
Use: "export {client} [--category category] [--tag tag] [--filter filter] [infoHash]...",
Annotations: map[string]string{"cobra-prompt-dynamic-suggestions": "export"},
Short: "Export and download torrents of client to .torrent files.",
Long: `Export and download torrents of client to .torrent files.
[infoHash]...: infoHash list of torrents. It's possible to use state filter to target multiple torrents:
_all, _active, _done, _undone, _downloading, _seeding, _paused, _completed, _error.
Specially, use a single "-" as args to read infoHash list from stdin, delimited by blanks.
To set the filenames of downloaded torrents, use --rename <name> flag,
which supports the following variable placeholders:
* [size] : Torrent size
* [infohash] : Torrent infohash
* [infohash16] : The first 16 chars of torrent infohash
* [category] : Torrent category
* [name] : Torrent name
* [name128] : The prefix of torrent name which is at max 128 bytes
Note it will overwrite any existing file on disk with the same name`,
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.OnlyValidArgs),
RunE: export,
}

var (
category = ""
tag = ""
filter = ""
downloadDir = ""
rename = ""
)

func init() {
command.Flags().StringVarP(&filter, "filter", "", "", "Filter torrents by name")
command.Flags().StringVarP(&category, "category", "", "", "Filter torrents by category")
command.Flags().StringVarP(&tag, "tag", "", "",
"Filter torrents by tag. Comma-separated list. Torrent which tags contain any one in the list matches")
command.Flags().StringVarP(&downloadDir, "download-dir", "", ".", `Set the download dir of exported torrents`)
command.Flags().StringVarP(&rename, "rename", "", "[name128].[infohash16].torrent",
"Set the name of downloaded torrents (supports variables)")
cmd.RootCmd.AddCommand(command)
}

func export(cmd *cobra.Command, args []string) error {
clientName := args[0]
infoHashes := args[1:]
if category == "" && tag == "" && filter == "" {
if len(infoHashes) == 0 {
return fmt.Errorf("you must provide at least a condition flag or hashFilter")
}
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
} else {
infoHashes = data
}
}
}
clientInstance, err := client.CreateClient(clientName)
if err != nil {
return fmt.Errorf("failed to create client: %v", err)
}

torrents, err := client.QueryTorrents(clientInstance, category, tag, filter, infoHashes...)
if err != nil {
return err
}
errorCnt := int64(0)
cntAll := len(torrents)
for i, torrent := range torrents {
content, err := clientInstance.ExportTorrentFile(torrent.InfoHash)
if err != nil {
fmt.Printf("✕ %s : failed to export %s: %v (%d/%d)\n", torrent.InfoHash, torrent.Name, err, i+1, cntAll)
errorCnt++
continue
}
filepath := path.Join(downloadDir, torrentutil.RenameExportedTorrent(torrent, rename))
if err := os.WriteFile(filepath, content, 0600); err != nil {
fmt.Printf("✕ %s : failed to save to %s: %v (%d/%d)\n", torrent.InfoHash, filepath, err, i+1, cntAll)
errorCnt++
} else {
fmt.Printf("✓ %s : saved to %s (%d/%d)\n", torrent.InfoHash, filepath, i+1, cntAll)
}
}
if errorCnt > 0 {
return fmt.Errorf("%d errors", errorCnt)
}
return nil
}
24 changes: 24 additions & 0 deletions cmd/export/suggest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package export

import (
"github.com/c-bata/go-prompt"

"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/cmd/shell/suggest"
)

func init() {
cmd.AddShellCompletion("export", func(document *prompt.Document) []prompt.Suggest {
info := suggest.Parse(document)
if info.LastArgIndex < 1 {
return nil
}
if info.LastArgIsFlag {
return nil
}
if info.LastArgIndex == 1 {
return suggest.ClientArg(info.MatchingPrefix)
}
return suggest.InfoHashOrFilterArg(info.MatchingPrefix, info.Args[1])
})
}
8 changes: 2 additions & 6 deletions cmd/pause/pause.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import (

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
)

var command = &cobra.Command{
Expand Down Expand Up @@ -46,10 +45,7 @@ func pause(cmd *cobra.Command, args []string) error {
return fmt.Errorf("you must provide at least a condition flag or hashFilter")
}
if len(infoHashes) == 1 && infoHashes[0] == "-" {
if config.InShell {
return fmt.Errorf(`"-" arg can not be used in shell`)
}
if data, err := util.ReadArgsFromStdin(); err != nil {
if data, err := helper.ReadArgsFromStdin(); err != nil {
return fmt.Errorf("failed to parse stdin to info hashes: %v", err)
} else if len(data) == 0 {
return nil
Expand Down

0 comments on commit f604364

Please sign in to comment.