Skip to content

Commit

Permalink
add xseedadd cmd; parsetorrent and some other cmds now also accept to…
Browse files Browse the repository at this point in the history
…rrent url arg
  • Loading branch information
sagan committed Mar 1, 2024
1 parent 53013c2 commit d2a7118
Show file tree
Hide file tree
Showing 29 changed files with 564 additions and 340 deletions.
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ ptool <command> args... [flags]
- parsetorrent : 显示种子(torrent)文件信息。
- verifytorrent : 测试种子(torrent)文件与硬盘上的文件内容一致。
- partialdownload : 拆包下载。
- xseedadd : 手动添加辅种种子到客户端。
- cookiecloud : 使用 [CookieCloud](https://github.com/easychen/CookieCloud) 同步站点的 Cookies 或导入站点。
- sites : 显示本程序内置支持的所有 PT 站点列表。
- config : 显示当前 ptool.toml 配置文件信息。
Expand Down Expand Up @@ -386,6 +387,8 @@ ptool add local "https://kp.m-team.cc/download.php?id=488424"

以上几条命令均可以将 M-Team 站点上 ID 为 [488424](https://kp.m-team.cc/details.php?id=488424&hit=1) 的种子添加到 "local" BT 客户端。

参数也支持传入公开 BT 网站的种子下载链接或 `magnet:` 磁力链接地址。

### 下载站点的种子

```
Expand Down Expand Up @@ -439,13 +442,17 @@ ptool batchdl <site> --action add --add-client local
### 显示种子文件信息 (parsetorrent)

```
ptool parsetorrent file.torrent...
ptool parsetorrent <torrentFileNameOrIdOrUrl>...
```

显示本地硬盘里的种子文件的元信息
显示种子文件的元信息。参数是本地硬盘里的种子文件名,或站点的种子 id 或 url(参考 "add" 命令说明)

### 校验种子文件与硬盘内容是否一致 (verifytorrent)

```
ptool verifytorrent <torrentFileNameOrIdOrUrl>...
```

示例:

```
Expand All @@ -457,7 +464,8 @@ ptool verifytorrent MyTorrent.torrent --content-path D:\Downloads\MyTorrent --ch
参数

- `--save-path` : 种子内容保存路径(下载文件夹)。可以用于校验多个 torrent 文件。
- `--content-path` : 种子内容路径(root folder 或单文件种子的文件路径)。只能用于校验 1 个 torrent 文件。必须且只能提供 `--save-path``-content-path` 两者中的其中 1 个参数。
- `--content-path` : 种子内容路径(root folder 或单文件种子的文件路径)。只能用于校验 1 个 torrent 文件。
- 必须且只能提供 `--save-path``-content-path` 两者中的其中 1 个参数。
- `--check` : 对硬盘上文件进行 hash 校验。如果不提供此参数,默认只对比文件元信息(文件名、文件大小)。

### 拆包下载 (partialdownload)
Expand All @@ -475,6 +483,14 @@ ptool partialdownload <client> <infoHash> --chunk-size 1TiB --chuck-index 0

该命令的设计目的不是用于刷流。而是用于使用 VPS 等硬盘空间有限的云服务器(分多次)下载体积非常大的单个种子,然后配合 rclone 将下载的文件直接上传到云盘。

### 手动添加辅种种子到客户端 (xseedadd)

```
ptool xseedadd <client> <torrentFileNameOrIdOrUrl>...
```

xseedadd 命令将提供的种子作为辅种种子添加到客户端。程序将在客户端里寻找与提供的种子元信息(文件名、文件大小)完全一致的目标种子,然后将提供的种子作为目标种子的辅种添加到客户端。如果客户端里没有找到匹配的目标种子,程序不会添加提供的种子到客户端。

### 同步 Cookies & 导入站点 (cookiecloud)

程序支持通过 [CookieCloud](https://github.com/easychen/CookieCloud) 服务器同步站点 Cookies 或导入站点。
Expand Down
23 changes: 13 additions & 10 deletions client/qbittorrent/qbittorrent.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,24 @@ func (qbclient *Client) AddTorrent(torrentContent []byte, option *client.Torrent
if err != nil {
return fmt.Errorf("login error: %v", err)
}

name := client.GenerateNameWithMeta(option.Name, meta)
body := new(bytes.Buffer)
mp := multipart.NewWriter(body)
defer mp.Close()
// see https://stackoverflow.com/questions/21130566/how-to-set-content-type-for-a-form-filed-using-multipart-in-go
// torrentPartWriter, _ := mp.CreateFormField("torrents")
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="torrents"; filename="file.torrent"`)
h.Set("Content-Type", "application/x-bittorrent")
torrentPartWriter, err := mp.CreatePart(h)
if err != nil {
return err
if util.IsTorrentUrl(string(torrentContent)) {
mp.WriteField("urls", string(torrentContent))
} else {
// see https://stackoverflow.com/questions/21130566/how-to-set-content-type-for-a-form-filed-using-multipart-in-go
// torrentPartWriter, _ := mp.CreateFormField("torrents")
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="torrents"; filename="file.torrent"`)
h.Set("Content-Type", "application/x-bittorrent")
torrentPartWriter, err := mp.CreatePart(h)
if err != nil {
return err
}
torrentPartWriter.Write(torrentContent)
}
torrentPartWriter.Write(torrentContent)
mp.WriteField("rename", name)
mp.WriteField("root_folder", "true")
if option != nil {
Expand Down
16 changes: 11 additions & 5 deletions client/transmission/transmission.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,23 @@ func (trclient *Client) GetTorrents(stateFilter string, category string, showAll

func (trclient *Client) AddTorrent(torrentContent []byte, option *client.TorrentOption, meta map[string]int64) error {
transmissionbt := trclient.client
torrentContentB64 := base64.StdEncoding.EncodeToString(torrentContent)
var downloadDir *string
if option.SavePath != "" {
downloadDir = &option.SavePath
}
// returned torrent will only have HashString, ID and Name fields set up.
torrent, err := transmissionbt.TorrentAdd(context.TODO(), transmissionrpc.TorrentAddPayload{
MetaInfo: &torrentContentB64,
payload := transmissionrpc.TorrentAddPayload{
Paused: &option.Pause,
DownloadDir: downloadDir,
})
}
if util.IsTorrentUrl(string(torrentContent)) {
url := string(torrentContent)
payload.Filename = &url
} else {
torrentContentB64 := base64.StdEncoding.EncodeToString(torrentContent)
payload.MetaInfo = &torrentContentB64
}
// returned torrent will only have HashString, ID and Name fields set up.
torrent, err := transmissionbt.TorrentAdd(context.TODO(), payload)
if err != nil {
return err
}
Expand Down
102 changes: 20 additions & 82 deletions cmd/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"fmt"
"io"
"os"
"path"
"strings"

"github.com/google/shlex"
log "github.com/sirupsen/logrus"
Expand All @@ -15,9 +13,8 @@ import (
"github.com/sagan/ptool/client"
"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/site"
"github.com/sagan/ptool/site/tpl"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/util/helper"
"github.com/sagan/ptool/util/torrentutil"
)

Expand All @@ -28,7 +25,8 @@ var command = &cobra.Command{
Short: "Add torrents to client.",
Long: `Add torrents to client.
Args is torrent list that each one could be a local filename (e.g. "*.torrent" or "[M-TEAM]CLANNAD.torrent"),
torrent id (e.g.: "mteam.488424"), or torrent url (e.g.: "https://kp.m-team.cc/details.php?id=488424").
site torrent id (e.g.: "mteam.488424") or url (e.g.: "https://kp.m-team.cc/details.php?id=488424").
Torrent url that does NOT belong to any site (e.g.: a public site url), as well as "magnet:" link, is also supported.
Use a single "-" as args to read torrent list from stdin, delimited by blanks,
as a special case, it also supports directly reading .torrent file contents from stdin.
Expand Down Expand Up @@ -84,7 +82,7 @@ func add(cmd *cobra.Command, args []string) error {
return fmt.Errorf("--rename-added and --delete-added flags are NOT compatible")
}
// directly read a torrent content from stdin.
var directTorrentContent []byte
stdinTorrentContents := []byte{}
torrents := util.ParseFilenameArgs(args[1:]...)
if len(torrents) == 1 && torrents[0] == "-" {
if config.InShell {
Expand All @@ -95,7 +93,7 @@ func add(cmd *cobra.Command, args []string) error {
} else if bytes.HasPrefix(stdin, []byte("d8:announce")) {
// Matches with .torrent file magic number.
// See: https://en.wikipedia.org/wiki/Torrent_file , https://en.wikipedia.org/wiki/Bencode .
directTorrentContent = stdin
stdinTorrentContents = stdin
} else if data, err := shlex.Split(string(stdin)); err != nil {
return fmt.Errorf("failed to parse stdin to tokens: %v", err)
} else {
Expand All @@ -116,95 +114,34 @@ func add(cmd *cobra.Command, args []string) error {
if addTags != "" {
fixedTags = util.SplitCsv(addTags)
}
domainSiteMap := map[string]string{}
siteInstanceMap := map[string]site.Site{}
errorCnt := int64(0)
cntAdded := int64(0)
sizeAdded := int64(0)
cntAll := len(torrents)

for i, torrent := range torrents {
var siteName string
var filename string // original torrent filename
var content []byte
var id string // site torrent id
var err error
var hr bool
isLocal := forceLocal || torrent == "-" || !util.IsUrl(torrent) && strings.HasSuffix(torrent, ".torrent")
if !isLocal {
// site torrent
siteName = defaultSite
if !util.IsUrl(torrent) {
if i := strings.Index(torrent, "."); i != -1 {
siteName = torrent[:i]
}
} else {
domain := util.GetUrlDomain(torrent)
if domain == "" {
fmt.Printf("✕add (%d/%d) %s: failed to parse domain", i+1, cntAll, torrent)
errorCnt++
continue
}
sitename := ""
ok := false
if sitename, ok = domainSiteMap[domain]; !ok {
domainSiteMap[domain], err = tpl.GuessSiteByDomain(domain, defaultSite)
if err != nil {
log.Warnf("Failed to find match site for %s: %v", domain, err)
}
sitename = domainSiteMap[domain]
}
if sitename == "" {
log.Warnf("Torrent %s: url does not match any site. will use provided default site", torrent)
} else {
siteName = sitename
}
}
if siteName == "" {
fmt.Printf("✕add (%d/%d) %s: no site found or provided\n", i+1, cntAll, torrent)
// handle as a special case
if util.IsPureTorrentUrl(torrent) {
option.Category = addCategory
option.Tags = fixedTags
if err = clientInstance.AddTorrent([]byte(torrent), option, nil); err != nil {
fmt.Printf("✕add (%d/%d) %s: failed to add to client: %v\n", i+1, cntAll, torrent, err)
errorCnt++
continue
}
if siteInstanceMap[siteName] == nil {
siteInstance, err := site.CreateSite(siteName)
if err != nil {
return fmt.Errorf("failed to create site %s: %v", siteName, err)
}
siteInstanceMap[siteName] = siteInstance
}
siteInstance := siteInstanceMap[siteName]
hr = siteInstance.GetSiteConfig().GlobalHnR
content, filename, id, err = siteInstance.DownloadTorrent(torrent)
} else {
if torrent == "-" {
filename = ""
content = directTorrentContent
} else if strings.HasSuffix(torrent, ".added") {
fmt.Printf("-skip (%d/%d) %s\n", i+1, cntAll, torrent)
continue
} else {
filename = path.Base(torrent)
content, err = os.ReadFile(torrent)
fmt.Printf("✓add (%d/%d) %s\n", i+1, cntAll, torrent)
}
}

if err != nil {
fmt.Printf("✕add (%d/%d) %s (site=%s): failed to fetch: %v\n", i+1, cntAll, torrent, siteName, err)
errorCnt++
continue
}
tinfo, err := torrentutil.ParseTorrent(content, 99)
content, tinfo, siteInstance, siteName, filename, id, err :=
helper.GetTorrentContent(torrent, defaultSite, forceLocal, false, stdinTorrentContents)
if err != nil {
fmt.Printf("✕add (%d/%d) %s (site=%s): failed to parse torrent: %v\n", i+1, cntAll, torrent, siteName, err)
fmt.Printf("✕add (%d/%d) %s: %v\n", i+1, cntAll, torrent, err)
errorCnt++
continue
}
if siteName == "" {
if sitename, err := tpl.GuessSiteByTrackers(tinfo.Trackers, defaultSite); err != nil {
log.Warnf("Failed to find match site for %s by trackers: %v", torrent, err)
} else {
siteName = sitename
}
hr := false
if siteInstance != nil {
hr = siteInstance.GetSiteConfig().GlobalHnR
}
if addCategoryAuto {
if siteName != "" {
Expand All @@ -217,6 +154,7 @@ func add(cmd *cobra.Command, args []string) error {
} else {
option.Category = addCategory
}
option.Tags = nil
if siteName != "" {
option.Tags = append(option.Tags, client.GenerateTorrentTagFromSite(siteName))
}
Expand All @@ -234,7 +172,7 @@ func add(cmd *cobra.Command, args []string) error {
errorCnt++
continue
}
if isLocal && torrent != "-" {
if siteInstance == nil && torrent != "-" {
if renameAdded {
if err := os.Rename(torrent, torrent+".added"); err != nil {
log.Debugf("Failed to rename %s to *.added: %v // %s", torrent, err, tinfo.ContentPath)
Expand Down
1 change: 1 addition & 0 deletions cmd/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ import (
_ "github.com/sagan/ptool/cmd/tidyup"
_ "github.com/sagan/ptool/cmd/verifytorrent"
_ "github.com/sagan/ptool/cmd/versioncmd"
_ "github.com/sagan/ptool/cmd/xseedadd"
_ "github.com/sagan/ptool/cmd/xseedcheck"
)
4 changes: 2 additions & 2 deletions cmd/configcmd/configcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ func configcmd(cmd *cobra.Command, args []string) error {
fmt.Printf("Config file: %s%c%s\n", config.ConfigDir, filepath.Separator, config.ConfigFile)
if _, err := os.Stat(path.Join(config.ConfigDir, config.ConfigFile)); err != nil {
if os.IsNotExist(err) {
fmt.Printf(`<config file does NOT exists, run "ptool config create" to create it>` + "\n")
fmt.Printf(`<config file does NOT exist, run "ptool config create" to create it>` + "\n")
} else {
fmt.Printf("<config file can not be accessed: %v>\n", err)
fmt.Printf("<config file is NOT accessible: %v>\n", err)
}
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/cookiecloud/importsites/importsites.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ func importsites(cmd *cobra.Command, args []string) error {
err := config.Set()
if err == nil {
fmt.Printf("Successfully update config file %s\n", configFile)
return nil
} else {
log.Fatalf("Failed to update config file %s : %v", configFile, err)
return fmt.Errorf("failed to update config file %s : %v", configFile, err)
}
} else {
fmt.Printf("!No new sites found in cookiecloud datas\n")
Expand Down
3 changes: 2 additions & 1 deletion cmd/cookiecloud/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ func sync(cmd *cobra.Command, args []string) error {
err := config.Set()
if err == nil {
fmt.Printf("Successfully update config file %s\n", configFile)
return nil
} else {
log.Fatalf("Failed to update config file %s : %v", configFile, err)
return fmt.Errorf("failed to update config file %s : %v", configFile, err)
}
} else {
fmt.Printf("!No new cookie found for any site\n")
Expand Down

0 comments on commit d2a7118

Please sign in to comment.