Skip to content

Commit

Permalink
improve output of some cmds
Browse files Browse the repository at this point in the history
  • Loading branch information
sagan committed Feb 21, 2024
1 parent 862a575 commit 4b0b002
Show file tree
Hide file tree
Showing 16 changed files with 165 additions and 146 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@
- 目前支持的 BT 客户端: qBittorrent v4.1+ / Transmission (<= v3.0)。
- 推荐使用 qBittorrent。Transmission 客户端未充分测试。
- 目前支持的 PT 站点:绝大部分使用 nexusphp 的网站。
- 测试过支持的站点:M-Team(馒头)、柠檬、U2、冬樱、红叶、聆音、铂金家、若干不可说的站点
- 未列出的大部分 np 站点应该也支持。除了个别魔改 np 很厉害的站点可能不支持
- 测试过支持的站点:M-Team(馒头)、U2、冬樱、红叶、聆音、铂金家、若干不可说的站点等
- 未列出的大部分 np 站点应该也支持。除了个别魔改 np 很厉害的站点可能有问题
- 刷流功能(brush):
- 不依赖 RSS。直接抓取站点页面上最新的种子。
- 无需配置选种规则。自动跳过非免费的和有 HR 的种子;自动筛选适合刷流的种子。
- 无需配置删种规则。自动删除已无刷流价值的种子;自动删除免费时间到期并且尚未下载完成的种子;硬盘空间不足时也会自动删种。
- 自动模仿浏览器访问 PT 站点,能够绕过大多数站点的 CF 盾 (impersonate 特性)。

## 下载

- [开发版本](https://ci.appveyor.com/project/sagan/ptool/build/artifacts) (根据 master 分支最新代码自动构建)
- [稳定版本](https://github.com/sagan/ptool/releases)

## 快速开始(刷流)

下载本程序的可执行文件 ptool (Linux) 或 ptool.exe (Windows) 放到任意目录,在同目录下创建名为 "ptool.toml" 的配置文件,内容示例如下:
将本程序的可执行文件 ptool (Linux) 或 ptool.exe (Windows) 放到任意目录,在同目录下创建名为 "ptool.toml" 的配置文件,内容示例如下:

```toml
[[clients]]
Expand Down Expand Up @@ -238,12 +243,12 @@ ptool <command> <client> [flags] [<infoHash>...]

`<infoHash>` 参数为指定的 BT 客户端里需要操作的种子的 infoHash 列表。也可以使用以下特殊值参数操作多个种子(delete 命令根据除 infoHash 以外的条件删除种子时需要二次确认):

- \_all : 所有种子
- \_done : 所有已下载完成的种子(无论是否正在做种)(\_seeding | \_completed)
- \_undone : 所有未下载完成的种子(\_downloading | \_paused)
- \_active : 当前正在活动(上传或下载)的种子
- \_error : 状态为“出错”的种子
- \_downloading / \_seeding / \_paused / \_completed : 状态为正在下载 / 做种 / 暂停下载 / 下载完成(但未做种)的种子
- `_all` : 所有种子
- `_done` : 所有已下载完成的种子(无论是否正在做种)(`_seeding` | `_completed`)
- `_undone` : 所有未下载完成的种子(`_downloading` | `_paused`)
- `_active` : 当前正在活动(上传或下载)的种子
- `_error` : 状态为“出错”的种子
- `_downloading` / `_seeding` / `_paused` / `_completed` : 状态为正在下载 / 做种 / 暂停下载 / 下载完成(但未做种)的种子

也可以使用以下条件 flags 筛选种子:

Expand Down Expand Up @@ -388,9 +393,9 @@ ptool dltorrent <torrentIdOrUrl>...
ptool search <sites> <keyword>
```

`<sites>` 参数为需要所搜索的 PT 站点,可以使用 "," 分割提供多个站点。可以使用 "\_all" 搜索所有已配置的 PT 站点。
`<sites>` 参数为需要所搜索的 PT 站点,可以使用 "," 分割提供多个站点。可以使用 `_all` 搜索所有已配置的 PT 站点。

可以用 `ptool add` 命令将搜索结果列表中的种子添加到 BT 客户端。
使用 `ptool add` 命令将搜索结果列表中的种子添加到 BT 客户端。

### 批量下载种子 (batchdl)

Expand All @@ -403,7 +408,6 @@ ptool batchdl <site>
# 下载找到的种子到当前目录
ptool batchdl <site> --action download
# 直接将种子添加到 "local" BT 客户端里
ptool batchdl <site> --action add --add-client local
```
Expand Down
11 changes: 6 additions & 5 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
type Torrent struct {
InfoHash string
Name string
TrackerDomain string
TrackerDomain string // e.g.: tracker.m-team.cc
TrackerBaseDomain string // e.g.: m-team.cc
Tracker string
State string // simplified state: seeding|downloading|completed|paused|checking|error|unknown
LowLevelState string // original state value returned by bt client
Expand Down Expand Up @@ -421,14 +422,14 @@ func PrintTorrents(torrents []Torrent, filter string, showSum int64, dense bool)
if width < config.CLIENT_TORRENTS_WIDTH {
width = config.CLIENT_TORRENTS_WIDTH
}
widthExcludingName := 109 // 40+6+5+6+6+5+5+20+8*2
widthExcludingName := 105 // 40+6+5+6+6+5+5+16+8*2
widthName := width - widthExcludingName
cnt := int64(0)
var cntPaused, cntDownloading, cntSeeding, cntCompleted, cntOthers int64
size := int64(0)
sizeUnfinished := int64(0)
if showSum < 2 {
fmt.Printf("%-*s %-40s %-6s %-5s %-6s %-6s %-5s %-5s %-20s\n",
fmt.Printf("%-*s %-40s %-6s %-5s %-6s %-6s %-5s %-5s %-16s\n",
widthName, "Name", "InfoHash", "Size", "State", "↓S(/s)", "↑S(/s)", "Seeds", "Peers", "Tracker")
}
for _, torrent := range torrents {
Expand All @@ -454,15 +455,15 @@ func PrintTorrents(torrents []Torrent, filter string, showSum int64, dense bool)
continue
}
remain := util.PrintStringInWidth(torrent.Name, int64(widthName), true)
fmt.Printf(" %-40s %-6s %-5s %-6s %-6s %-5d %-5d %-20s\n",
fmt.Printf(" %-40s %-6s %-5s %-6s %-6s %-5d %-5d %-16s\n",
torrent.InfoHash,
util.BytesSizeAround(float64(torrent.Size)),
torrent.StateIconText(),
util.BytesSizeAround(float64(torrent.DownloadSpeed)),
util.BytesSizeAround(float64(torrent.UploadSpeed)),
torrent.Seeders,
torrent.Leechers,
torrent.TrackerDomain,
torrent.TrackerBaseDomain, // 目前遇到的tracker域名最长的: "wintersakura.net"
)
if dense {
for {
Expand Down
1 change: 1 addition & 0 deletions client/qbittorrent/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func (qbtorrent *apiTorrentInfo) ToTorrent() *client.Torrent {
InfoHash: qbtorrent.Hash,
Name: qbtorrent.Name,
TrackerDomain: util.ParseUrlHostname(qbtorrent.Tracker),
TrackerBaseDomain: util.GetUrlDomain(qbtorrent.Tracker),
Tracker: qbtorrent.Tracker,
State: qbtorrent.ToTorrentState(),
LowLevelState: qbtorrent.State,
Expand Down
1 change: 1 addition & 0 deletions client/transmission/transmission.go
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@ func tr2Torrent(trtorrent *transmissionrpc.Torrent) *client.Torrent {
InfoHash: *trtorrent.HashString,
Name: *trtorrent.Name,
TrackerDomain: util.ParseUrlHostname(tracker),
TrackerBaseDomain: util.GetUrlDomain(tracker),
Tracker: tracker,
State: tr2State(trtorrent),
LowLevelState: fmt.Sprint(trtorrent.Status),
Expand Down
2 changes: 1 addition & 1 deletion cmd/batchdl/batchdl.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ var (
func init() {
command.Flags().BoolVarP(&onePage, "one-page", "", false, "Only fetch one page torrents")
command.Flags().BoolVarP(&addPaused, "add-paused", "", false, "Add torrents to client in paused state")
command.Flags().BoolVarP(&dense, "dense", "", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&dense, "dense", "d", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&freeOnly, "free", "", false, "Skip none-free torrent")
command.Flags().BoolVarP(&noPaid, "no-paid", "", false, "Skip paid (use bonus points) torrent")
command.Flags().BoolVarP(&noNeutral, "no-neutral", "", false, "Skip neutral (do not count uploading & downloading & seeding bonus) torrent")
Expand Down
21 changes: 17 additions & 4 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gofrs/flock"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/sagan/ptool/client"
"github.com/sagan/ptool/config"
Expand Down Expand Up @@ -62,8 +63,11 @@ func Execute() {
config.ConfigType = configExt[1:]
}
logLevel := 3 + config.VerboseLevel
isTty := term.IsTerminal(int(os.Stdout.Fd()))
width, height, _ := term.GetSize(int(os.Stdout.Fd()))
log.SetLevel(log.Level(logLevel))
log.Debugf("ptool start: %v", os.Args)
log.Debugf("tty=%t, width=%d, height=%d", isTty, width, height)
log.Infof("config file: %s/%s", config.ConfigDir, config.ConfigFile)
if config.LockFile != "" {
log.Debugf("Locking file: %s", config.LockFile)
Expand Down Expand Up @@ -124,10 +128,19 @@ func init() {
config.DefaultConfigFile = configFile

// global flags
RootCmd.PersistentFlags().BoolVarP(&config.Fork, "fork", "", false, "Enables a daemon mode that runs the ptool process in the background (detached from current terminal). The current stdout / stderr will still be used so you may want to redirect them to files using pipe. It only works on Linux platform")
RootCmd.PersistentFlags().BoolVarP(&config.LockOrExit, "lock-or-exit", "", false, "Used with --lock flag. If failed to acquire lock, exit 1 immediately instead of waiting")
RootCmd.PersistentFlags().StringVarP(&config.ConfigFile, "config", "", config.DefaultConfigFile, "Config file ([ptool.toml])")
RootCmd.PersistentFlags().StringVarP(&config.LockFile, "lock", "", "", "Lock filename. If set, ptool will acquire the lock on the file before executing command. It is intended to be used to prevent multiple invocations of ptool process at the same time. If the lock file does not exist, it will be created automatically. However, it will NOT be deleted after ptool process exits")
RootCmd.PersistentFlags().BoolVarP(&config.Fork, "fork", "", false,
"Enables a daemon mode that runs the ptool process in the background (detached from current terminal). "+
"The current stdout / stderr will still be used so you may want to redirect them to files using pipe. "+
"It only works on Linux platform")
RootCmd.PersistentFlags().BoolVarP(&config.LockOrExit, "lock-or-exit", "", false,
"Used with --lock flag. If failed to acquire lock, exit 1 immediately instead of waiting")
RootCmd.PersistentFlags().StringVarP(&config.ConfigFile, "config", "", config.DefaultConfigFile,
"Config file ([ptool.toml])")
RootCmd.PersistentFlags().StringVarP(&config.LockFile, "lock", "", "",
"Lock filename. If set, ptool will acquire the lock on the file before executing command. "+
"It is intended to be used to prevent multiple invocations of ptool process at the same time. "+
"If the lock file does not exist, it will be created automatically. "+
"However, it will NOT be deleted after ptool process exits")
RootCmd.PersistentFlags().CountVarP(&config.VerboseLevel, "verbose", "v", "verbose (-v, -vv, -vvv)")
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ var (
)

func init() {
command.Flags().BoolVarP(&dense, "dense", "", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&dense, "dense", "d", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&largestFlag, "largest", "l", false, "Sort search result by torrent size in desc order")
command.Flags().BoolVarP(&newestFlag, "newest", "n", false, "Sort search result by torrent time in desc order")
command.Flags().BoolVarP(&showJson, "json", "", false, "Show output in json format")
Expand Down
6 changes: 3 additions & 3 deletions cmd/shell/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ var lsCmd = &cobra.Command{
}
info, err := file.Info()
if err != nil {
fmt.Printf("%-1s %10s %19s %-s\n", flag, "<error>", "<error>", file.Name())
fmt.Printf("%-1s %6s %19s %-s\n", flag, "!error", "<error>", file.Name())
} else {
fmt.Printf("%-1s %10s %19s %-s\n",
fmt.Printf("%-1s %6s %19s %-s\n",
flag,
util.BytesSize(float64(info.Size())),
util.BytesSizeAround(float64(info.Size())),
util.FormatTime(info.ModTime().Unix()),
util.QuoteFilename(file.Name()))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/show/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ var (
func init() {
command.Flags().Int64VarP(&maxTorrents, "max-torrents", "", -1,
"Show at most this number of torrents. -1 == no limit")
command.Flags().BoolVarP(&dense, "dense", "", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&dense, "dense", "d", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&largestFlag, "largest", "l", false,
"Show largest torrents first. Equavalent with '--sort size --order desc'")
command.Flags().BoolVarP(&showAll, "all", "a", false, "Show all torrents. Equavalent with pass a '_all' arg")
Expand Down
78 changes: 42 additions & 36 deletions cmd/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,29 @@ var (
)

var command = &cobra.Command{
Use: "status [client | site | group]... [-a]",
Use: "status [client | site | group]... [-a | -c | -s]",
// Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.OnlyValidArgs),
Annotations: map[string]string{"cobra-prompt-dynamic-suggestions": "status"},
Short: "Show clients or sites status.",
Long: `Show clients or sites status.
[client | site | group]: name of a client, site or group, or "_all" which means all sites.
[client | site | group]: name of a client, site or group.
`,
RunE: status,
}

func init() {
command.Flags().StringVarP(&filter, "filter", "", "", "Filter client torrents by name")
command.Flags().StringVarP(&filter, "filter", "", "", "Filter torrents by name")
command.Flags().StringVarP(&category, "category", "", "", "Filter client torrents by category")
command.Flags().BoolVarP(&dense, "dense", "", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&dense, "dense", "d", false, "Dense mode: show full torrent title & subtitle")
command.Flags().BoolVarP(&showAll, "all", "a", false, "Show all clients and sites")
command.Flags().BoolVarP(&showAllClients, "clients", "c", false, "Show all clients")
command.Flags().BoolVarP(&showAllSites, "sites", "s", false, "Show all sites")
command.Flags().BoolVarP(&showTorrents, "torrents", "t", false,
"Show torrents (active torrents for client / latest torrents for site)")
command.Flags().BoolVarP(&showFull, "full", "f", false, "Show full info of each client or site")
command.Flags().BoolVarP(&showScore, "score", "", false, "Show brush score of site torrents")
command.Flags().BoolVarP(&largestFlag, "largest", "l", false, `Sort site torrents by size in desc order"`)
command.Flags().BoolVarP(&newestFlag, "newest", "n", false,
`Sort site torrents by time in desc order (newest first)"`)
command.Flags().BoolVarP(&largestFlag, "largest", "l", false, `Sort torrents by size in desc order"`)
command.Flags().BoolVarP(&newestFlag, "newest", "n", false, `Sort torrents by time in desc order"`)
cmd.RootCmd.AddCommand(command)
}

Expand Down Expand Up @@ -133,71 +132,80 @@ func status(cmd *cobra.Command, args []string) error {
return indexA < indexB
})

// type, name, ↑info, ↓info, others
var format = "%-6s %-15s %-27s %-27s %-s\n"
errorsStr := ""
for i, response := range responses {
for _, response := range responses {
if response.Kind == 1 {
if response.Error != nil {
errorsStr += fmt.Sprintf("Error get client %s status: error=%v\n", response.Name, response.Error)
errorCnt++
}
if response.ClientStatus != nil {
fmt.Printf("%-6s %-13s %-25s %-25s %-25s",
info := fmt.Sprintf("FreeSpace: %s; Unfinished(All/DL): %s/%s",
util.BytesSizeAround(float64(response.ClientStatus.FreeSpaceOnDisk)),
util.BytesSizeAround(float64(response.ClientStatus.UnfinishedSize)),
util.BytesSizeAround(float64(response.ClientStatus.UnfinishedDownloadingSize)),
)
if len(response.ClientTorrents) > 0 {
info += fmt.Sprintf("; Torrents: %d", len(response.ClientTorrents))
}
fmt.Printf(format,
"Client",
response.Name,
fmt.Sprintf("↑Spd/Lmt: %s / %s/s", util.BytesSize(float64(response.ClientStatus.UploadSpeed)),
util.BytesSize(float64(response.ClientStatus.UploadSpeedLimit))),
util.BytesSizeAround(float64(response.ClientStatus.UploadSpeedLimit))),
fmt.Sprintf("↓Spd/Lmt: %s / %s/s", util.BytesSize(float64(response.ClientStatus.DownloadSpeed)),
util.BytesSize(float64(response.ClientStatus.DownloadSpeedLimit))),
fmt.Sprintf("FreeSpace: %s; Unfinished(All/DL): %s/%s",
util.BytesSize(float64(response.ClientStatus.FreeSpaceOnDisk)),
util.BytesSize(float64(response.ClientStatus.UnfinishedSize)),
util.BytesSize(float64(response.ClientStatus.UnfinishedDownloadingSize)),
),
util.BytesSizeAround(float64(response.ClientStatus.DownloadSpeedLimit))),
info,
)
if len(response.ClientTorrents) > 0 {
fmt.Printf(" Torrents: %d", len(response.ClientTorrents))
}
fmt.Printf("\n")
} else {
fmt.Printf("%-6s %-13s %-25s %-25s %-25s\n",
fmt.Printf(format,
"Client",
response.Name,
"-",
"-",
"// failed to get status",
"// <error>",
)
}
if response.ClientTorrents != nil {
fmt.Printf("\n")
client.PrintTorrents(response.ClientTorrents, filter, 0, dense)
if i != len(responses)-1 {
fmt.Printf("\n")
if largestFlag {
sort.Slice(response.ClientTorrents, func(i, j int) bool {
return response.ClientTorrents[i].Size > response.ClientTorrents[j].Size
})
} else if newestFlag {
sort.Slice(response.ClientTorrents, func(i, j int) bool {
return response.ClientTorrents[i].Atime > response.ClientTorrents[j].Atime
})
}
client.PrintTorrents(response.ClientTorrents, filter, 0, dense)
fmt.Printf("\n")
}
} else if response.Kind == 2 {
if response.Error != nil {
errorsStr += fmt.Sprintf("Error get site %s status: error=%v\n", response.Name, response.Error)
errorCnt++
}
if response.SiteStatus != nil {
fmt.Printf("%-6s %-13s %-25s %-25s %-25s",
info := fmt.Sprintf("UserName: %s", response.SiteStatus.UserName)
if len(response.SiteTorrents) > 0 {
info += fmt.Sprintf("; Torrents: %d", len(response.SiteTorrents))
}
fmt.Printf(format,
"Site",
response.Name,
fmt.Sprintf("↑: %s", util.BytesSize(float64(response.SiteStatus.UserUploaded))),
fmt.Sprintf("↓: %s", util.BytesSize(float64(response.SiteStatus.UserDownloaded))),
fmt.Sprintf("UserName: %s", response.SiteStatus.UserName),
info,
)
if len(response.SiteTorrents) > 0 {
fmt.Printf(" Torrents: %d", len(response.SiteTorrents))
}
fmt.Printf("\n")
} else {
fmt.Printf("%-6s %-13s %-25s %-25s %-25s\n",
fmt.Printf(format,
"Site",
response.Name,
"-",
"-",
"// failed to get status",
"// <error>",
)
}
if response.SiteTorrents != nil {
Expand All @@ -212,9 +220,7 @@ func status(cmd *cobra.Command, args []string) error {
})
}
site.PrintTorrents(response.SiteTorrents, filter, now, false, dense, response.SiteTorrentScores)
if i != len(responses)-1 {
fmt.Printf("\n")
}
fmt.Printf("\n")
}
}
}
Expand Down

0 comments on commit 4b0b002

Please sign in to comment.