menu E4b9a6's blog
rss_feed
E4b9a6's blog
有善始者实繁,能克终者盖寡。

qBittorrent自动删除上传率低的种子

作者:E4b9a6, 创建:2024-12-03, 字数:6082, 已阅:199, 最后更新:2024-12-03

在使用 qBittorrent 的 RSS 订阅功能后,可以实现自动下载当前热门的新种子来赚上传量

但 QBitorrent 的自动删除的功能比较鸡肋,只有分享率大于某个值或者做种时间超过某个值时自动删除

抱着赚取上传量的念头这两个都不是很合适,查阅了资料,发现可以借助 Golang 的代码来调用 QBitorrent 的 API 来实现自定义删除规则

我需要的规则如下:

  • 当种子添加时间少于7天则不进行判定
  • 每次检查都要与上一次检查时留存的分享率进行对比,若低于某个值,则删除该种子以及种子所下载的文件

实现后,已将代码和打包好的二进制程序发布到 github ,仓库如下:

使用方法是从 Releases 中下载系统对应的客户端版本,然后执行如下:

Bash
qBittorrent-manager --username=admin --password=admin

在 crontab 中设置循环 7 天执行,则每 7 天检查一次上传率的增长情况,若当前轮次增长少于 0.5 则删除该种子以及种子所下载的文件

代码逻辑如下:

Go
package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "flag"
    "fmt"
    "net/http"
    "net/http/cookiejar"
    "os"
    "time"
)

// Torrent 结构体定义
type Torrent struct {
    Hash  string  `json:"hash"`
    Name  string  `json:"name"`
    Ratio float64 `json:"ratio"`
    Addon int64   `json:"added_on"`
}

// 登录并获取会话 cookie
func login(client *http.Client, qbURL, username, password string) error {
    loginData := fmt.Sprintf("username=%s&password=%s", username, password)
    req, err := http.NewRequest("POST", qbURL+"/api/v2/auth/login", bytes.NewBufferString(loginData))
    if err != nil {
    	return err
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
    	return fmt.Errorf("failed to login, status code: %d", resp.StatusCode)
    }

    fmt.Println("Logged in successfully.")
    return nil
}

// 获取当前所有种子的状态
func getTorrents(client *http.Client, qbURL string) ([]Torrent, error) {
    req, err := http.NewRequest("GET", qbURL+"/api/v2/torrents/info", nil)
    if err != nil {
    	return nil, err
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
    	return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
    	return nil, fmt.Errorf("failed to fetch torrents info, status code: %d", resp.StatusCode)
    }

    var torrents []Torrent
    if err := json.NewDecoder(resp.Body).Decode(&torrents); err != nil {
    	return nil, err
    }

    fmt.Println("Torrents information fetched successfully.")
    return torrents, nil
}

// 删除种子
func deleteTorrent(client *http.Client, qbURL, hash string) error {
    data := fmt.Sprintf("hashes=%s&deleteFiles=true", hash)
    req, err := http.NewRequest("POST", qbURL+"/api/v2/torrents/delete", bytes.NewBufferString(data))
    if err != nil {
    	return err
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
    	return fmt.Errorf("failed to delete torrent, status code: %d", resp.StatusCode)
    }

    return nil
}

func searchTorrentOnLocal(hash, recordFile string) (Torrent, error) {
    var torrent Torrent
    var torrents []Torrent
    if _, err := os.Stat(recordFile); os.IsNotExist(err) {
    	fmt.Println("No existing torrents found. Starting fresh.")
    	return torrent, err
    }

    data, err := os.ReadFile(recordFile)
    if err != nil {
    	return torrent, err
    }

    if err := json.Unmarshal(data, &torrents); err != nil {
    	return torrent, err
    }

    for _, torrent := range torrents {
    	if torrent.Hash == hash {
    		return torrent, nil
    	}
    }

    return torrent, errors.New("hash Not Found")
}

// 保存记录
func saveTorrentToLocal(torrents []Torrent, recordFile string) error {
    data, err := json.MarshalIndent(torrents, "", "  ")
    if err != nil {
    	return err
    }

    if err := os.WriteFile(recordFile, data, 0644); err != nil {
    	return err
    }

    fmt.Printf("Records saved to %s.\n", recordFile)
    return nil
}

func main() {
    // 定义命令行参数
    qbURL := flag.String("url", "http://localhost:8080", "The URL of the qBittorrent Web UI")
    username := flag.String("username", "admin", "The username for qBittorrent Web UI")
    password := flag.String("password", "adminadmin", "The password for qBittorrent Web UI")
    recordFile := flag.String("recordFile", "torrent-records.json", "The file to save torrent records")
    ratioIncrease := flag.Float64("ratioIncrease", 0.5, "The radio increases each time")
    protectionPeriod := flag.Int("protectionPeriod", 7, "The protection period for torrent")
    try := flag.Bool("try", false, "Display the deletion target without actually deleting the torrent")

    // 解析命令行参数
    flag.Parse()

    // 创建一个 cookie jar 来管理会话
    jar, err := cookiejar.New(nil)
    if err != nil {
    	fmt.Printf("Error creating cookie jar: %v\n", err)
    	return
    }

    // 创建 HTTP 客户端并设置 cookie jar
    client := &http.Client{
    	Jar: jar,
    }

    // 执行登录
    if err := login(client, *qbURL, *username, *password); err != nil {
    	fmt.Printf("Error: %v\n", err)
    	return
    }

    // 获取种子信息
    torrents, err := getTorrents(client, *qbURL)
    if err != nil {
    	fmt.Printf("Error: %v\n", err)
    	return
    }

    // 检查上传率
    nowTime := time.Now()
    for _, torrent := range torrents {
    	fmt.Printf("Processing torrent <%s>\n", torrent.Name)
    	if record, err := searchTorrentOnLocal(torrent.Hash, *recordFile); err != nil {
    		// 种子在本地没有记录则跳过
    		fmt.Printf("Torrent <%s> name not found\n", torrent.Name)
    		continue
    	} else {
    		// 保护期内的种子不会被删除
    		addTime := time.Unix(torrent.Addon, 0)
    		daysDiffDays := int(nowTime.Sub(addTime).Hours() / 24)
    		if daysDiffDays < *protectionPeriod {
    			fmt.Printf("Torrent <%s> not more than %d days\n", torrent.Name, *protectionPeriod)
    			continue
    		}

    		// 比对分享率增长
    		fmt.Printf("Torrent <%s> local record ratio %.2f and torrent current radio %.2f\n", record.Name, record.Ratio, torrent.Ratio)

    		if torrent.Ratio-record.Ratio < *ratioIncrease {
    			// 删除分享率上升不足的种子
    			fmt.Printf("Deleting torrent <%s> due to insufficient share ratio increase(radio increase %.2f).\n", torrent.Name, torrent.Ratio-record.Ratio)
    			if err := deleteTorrent(client, *qbURL, torrent.Hash); err != nil && !*try {
    				fmt.Printf("Failed to delete torrent %s error: %v\n", torrent.Name, err)
    			}
    		}
    	}
    }

    // 保存本次查询结果,方便下次查询比对分享率的增长情况
    if err := saveTorrentToLocal(torrents, *recordFile); err != nil {
    	fmt.Printf("Error saving torrents to local file: %v\n", err)
    	return
    }

    fmt.Println("All records processed and updated.")
}

[[replyMessage== null?"发表评论":"发表评论 @ " + replyMessage.m_author]]

account_circle
email
web_asset
textsms

评论列表([[messageResponse.total]])

还没有可以显示的留言...
gravatar
[[messageItem.m_author]] [[messageItem.m_author]]
[[messageItem.create_time]]
[[getEnviron(messageItem.m_environ)]]
[[subMessage.m_author]] [[subMessage.m_author]] @ [[subMessage.parent_message.m_author]] [[subMessage.parent_message.m_author]]
[[subMessage.create_time]]
[[getEnviron(messageItem.m_environ)]]