├── images ├── 20210502191832.png ├── 20210517010959.png └── 20210519125722.png ├── src ├── qbittorrent │ ├── pkg │ │ └── model │ │ │ ├── category.go │ │ │ ├── search_status.go │ │ │ ├── torrent_piece_state.go │ │ │ ├── search_results_paging.go │ │ │ ├── build_info.go │ │ │ ├── sync_peers_data.go │ │ │ ├── get_log_options.go │ │ │ ├── search_plugin.go │ │ │ ├── sync_main_data.go │ │ │ ├── search_result.go │ │ │ ├── torrent_tracker.go │ │ │ ├── peer.go │ │ │ ├── get_torrents_list_options.go │ │ │ ├── torrent_content.go │ │ │ ├── transfer_info.go │ │ │ ├── peer_log_entry.go │ │ │ ├── log_entry.go │ │ │ ├── rule_definition.go │ │ │ ├── add_torrents_options.go │ │ │ ├── server_state.go │ │ │ ├── torrent.go │ │ │ ├── torrent_properties.go │ │ │ └── preferences.go │ ├── client.go │ └── wrapper.go ├── nexus │ └── client.go ├── datebase │ └── client.go └── config │ └── client.go ├── .gitignore ├── goseeder.service ├── go.mod ├── README.md ├── .github └── workflows │ └── go.yml ├── main.go ├── stat.go ├── config-example.json └── go.sum /images/20210502191832.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickfox-taterli/goseeder/HEAD/images/20210502191832.png -------------------------------------------------------------------------------- /images/20210517010959.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickfox-taterli/goseeder/HEAD/images/20210517010959.png -------------------------------------------------------------------------------- /images/20210519125722.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickfox-taterli/goseeder/HEAD/images/20210519125722.png -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/category.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Category struct { 4 | Name string `json:"name"` 5 | SavePath string `json:"savePath"` 6 | } 7 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/search_status.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SearchStatus struct { 4 | ID int `json:"id"` 5 | Status string `json:"status"` 6 | Total int `json:"total"` 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen 10 | 11 | config.json -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/torrent_piece_state.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TorrentPieceState int 4 | 5 | const ( 6 | PieceStateNotDownloaded TorrentPieceState = iota 7 | PieceStateDownloading 8 | PieceStateDownloaded 9 | ) 10 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/search_results_paging.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SearchResultsPaging struct { 4 | Results []SearchResult `json:"results"` 5 | Status string `json:"status"` 6 | Total int `json:"total"` 7 | } 8 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/build_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type BuildInfo struct { 4 | QT string `json:"qt"` 5 | LibTorrent string `json:"libtorrent"` 6 | Boost string `json:"boost"` 7 | OpenSSL string `json:"openssl"` 8 | Bitness string `json:"bitness"` 9 | } 10 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/sync_peers_data.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SyncPeersData struct { 4 | FullUpdate bool `json:"full_update"` 5 | Peers map[string]Peer `json:"peers"` 6 | RID int `json:"rid"` 7 | ShowFlags bool `json:"show_flags"` 8 | } 9 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/get_log_options.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type GetLogOptions struct { 4 | Normal bool `url:"normal"` 5 | Info bool `url:"info"` 6 | Warning bool `url:"warning"` 7 | Critical bool `url:"critical"` 8 | LastKnownID int `url:"lastKnownId"` 9 | } 10 | -------------------------------------------------------------------------------- /goseeder.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Go Seeder Service 3 | After=network.target nss-lookup.target 4 | 5 | [Service] 6 | User=nobody 7 | NoNewPrivileges=true 8 | ExecStart=/usr/local/bin/goseeder 9 | Restart=on-failure 10 | RestartPreventExitStatus=23 11 | LimitNPROC=10000 12 | LimitNOFILE=1000000 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/search_plugin.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SearchPlugin struct { 4 | Enabled bool `json:"enabled"` 5 | FullName string `json:"fullName"` 6 | Name string `json:"name"` 7 | SupportedCategories []string `json:"supportedCategories"` 8 | URL string `json:"url"` 9 | Version string `json:"version"` 10 | } 11 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/sync_main_data.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SyncMainData struct { 4 | RID int `json:"rid"` 5 | FullUpdate bool `json:"full_update"` 6 | Torrents map[string]*Torrent `json:"torrents"` 7 | TorrentsRemoved []string `json:"torrents_removed"` 8 | Categories map[string]Category `json:"categories"` 9 | CategoriesRemoved map[string]Category `json:"categories_removed"` 10 | Queueing bool `json:"queueing"` 11 | ServerState ServerState `json:"server_state"` 12 | } 13 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/search_result.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SearchResult struct { 4 | // URL of the torrent's description page 5 | DescriptionLink string `json:"descrLink"` 6 | // Name of the file 7 | FileName string `json:"fileName"` 8 | // Size of the file in Bytes 9 | FileSize int `json:"fileSize"` 10 | // Torrent download link (usually either .torrent file or magnet link) 11 | FileUrl string `json:"fileUrl"` 12 | // Number of leechers 13 | NumLeechers int `json:"nbLeechers"` 14 | // int of seeders 15 | NumSeeders int `json:"nbSeeders"` 16 | // URL of the torrent site 17 | SiteUrl string `json:"siteUrl"` 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module seeder 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/codegangsta/cli v1.20.0 // indirect 7 | github.com/google/go-querystring v1.1.0 // indirect 8 | github.com/mattn/go-sqlite3 v1.14.12 // indirect 9 | github.com/mmcdole/gofeed v1.1.0 // indirect 10 | github.com/pkg/errors v0.9.1 // indirect 11 | github.com/robfig/cron v1.2.0 // indirect 12 | github.com/robfig/cron/v3 v3.0.1 // indirect 13 | github.com/sirupsen/logrus v1.8.1 // indirect 14 | github.com/tomcraven/gotable v0.0.0-20160801225336-eb315dabcfbd // indirect 15 | go.mongodb.org/mongo-driver v1.5.1 // indirect 16 | golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/torrent_tracker.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TorrentTracker struct { 4 | URL string `json:"url"` 5 | Status TrackerStatus `json:"status"` 6 | Tier interface{} `json:"tier"` 7 | NumPeers int `json:"num_peers"` 8 | NumSeeds int `json:"num_seeds"` 9 | NumLeeches int `json:"num_leeches"` 10 | NumDownloaded int `json:"num_downloaded"` 11 | Message string `json:"msg"` 12 | } 13 | 14 | type TrackerStatus int 15 | 16 | const ( 17 | TrackerStatusDisabled TrackerStatus = iota 18 | TrackerStatusNotContacted 19 | TrackerStatusWorking 20 | TrackerStatusUpdating 21 | TrackerStatusNotWorking 22 | ) 23 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/peer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Peer struct { 4 | Client string `json:"client"` 5 | Connection string `json:"connection"` 6 | Country string `json:"country"` 7 | CountryCode string `json:"country_code"` 8 | DLSpeed int `json:"dlSpeed"` 9 | Downloaded int `json:"downloaded"` 10 | Files string `json:"files"` 11 | Flags string `json:"flags"` 12 | FlagsDescription string `json:"flags_desc"` 13 | IP string `json:"ip"` 14 | Port int `json:"port"` 15 | Progress float64 `json:"progress"` 16 | Relevance int `json:"relevance"` 17 | ULSpeed int `json:"up_speed"` 18 | Uploaded int `json:"uploaded"` 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 自动PT工具(Go版) 2 | 3 | ------ 4 | 5 | **PTSCHOOL有严重的BUG(他们检测到我网速5万MB/s),请尽量避免使用,请参考下面图片.** 6 | ![](images/20210517010959.png) 7 | 8 | **工作图.** 9 | 10 | ![工作图](images/20210502191832.png) 11 | 12 | 适用于:多小鸡/盘都不大/只求不吃灰/流量可能还有限制/站点较多/非数据党 13 | 14 | 配置教程:[[BETA]改进版的PT自动工具][1] [参数调优][4] 15 | 16 | Time4VPS 大盘机当前正在搞活动,推荐上车:[15.00EUR/1年/256GB/超流量限速不停机/馒头不标记][2] 17 | 18 | Hostens 三年付最划算,推荐上车:[43.2USD/3年/256GB/超流量限速不停机/馒头不标记][3] 19 | 20 | ![工作图](images/20210519125722.png) 21 | 22 | [1]: https://www.taterli.com/7677/ "[BETA]改进版的PT自动工具" 23 | [2]: https://billing.time4vps.com/?cmd=cart&action=add&id=119&cycle=y&promocode=2021&utm_source=forum&utm_medium=offer&affid=5740 "15.00EUR/1年/256GB" 24 | [3]: https://www.hostens.com/?affid=1662 "43.2USD/3年/256GB" 25 | [4]: https://www.taterli.com/7785/ "参数调优" 26 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/get_torrents_list_options.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type GetTorrentListOptions struct { 4 | Filter TorrentListFilter `url:"filter,omitempty"` 5 | Category *string `url:"category,omitempty"` 6 | Sort string `url:"sort,omitempty"` 7 | Reverse bool `url:"reverse,omitempty"` 8 | Limit int `url:"limit,omitempty"` 9 | Offset int `url:"offset,omitempty"` 10 | Hashes string `url:"hashes,omitempty"` 11 | } 12 | 13 | type TorrentListFilter string 14 | 15 | const ( 16 | FilterAll TorrentListFilter = "all" 17 | FilterDownloading = "downloading" 18 | FilterCompleted = "completed" 19 | FilterPaused = "paused" 20 | FilterActive = "active" 21 | FilterInactive = "inactive" 22 | FilterResumed = "resumed" 23 | ) 24 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/torrent_content.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TorrentContent struct { 4 | // File name (including relative path) 5 | Name string `json:" name"` 6 | // File size (bytes) 7 | Size int `json:" size"` 8 | // File progress (percentage/100) 9 | Progress float64 `json:" progress"` 10 | // File priority. See possible values here below 11 | Priority TorrentPriority `json:" priority"` 12 | // True if file is seeding/complete 13 | IsSeed bool `json:" is_seed"` 14 | // The first number is the starting piece index and the second number is the ending piece index (inclusive) 15 | PieceRange []int `json:" piece_range"` 16 | // Percentage of file pieces currently available 17 | Availability float64 `json:" availability"` 18 | } 19 | 20 | type TorrentPriority int 21 | 22 | const ( 23 | PriorityDoNotDownload TorrentPriority = 0 24 | PriorityNormal = 1 25 | PriorityHigh = 6 26 | PriorityMaximum = 7 27 | ) 28 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/transfer_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TransferInfo struct { 4 | ConnectionStatus ConnectionStatus `json:"connection_status"` 5 | DhtNodes int `json:"dht_nodes"` 6 | DlInfoData int `json:"dl_info_data"` 7 | DlInfoSpeed int `json:"dl_info_speed"` 8 | DlRateLimit int `json:"dl_rate_limit"` 9 | UpInfoData int `json:"up_info_data"` 10 | UpInfoSpeed int `json:"up_info_speed"` 11 | UpRateLimit int `json:"up_rate_limit"` 12 | UseAltSpeedLimits bool `json:"use_alt_speed_limits"` 13 | Queueing bool `json:"queueing"` 14 | RefreshInterval int `json:"refresh_interval"` 15 | } 16 | 17 | type ConnectionStatus string 18 | 19 | const ( 20 | StatusConnected ConnectionStatus = "connected" 21 | StatusFirewalled = "firewalled" 22 | StatusDisconnected = "disconnected" 23 | ) 24 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/peer_log_entry.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type PeerLogEntry struct { 9 | ID int `json:"id"` 10 | IP string `json:"ip"` 11 | Timestamp time.Time `json:"timestamp"` 12 | Blocked bool `json:"blocked"` 13 | Reason string `json:"reason"` 14 | } 15 | 16 | func (l *PeerLogEntry) UnmarshalJSON(data []byte) error { 17 | var raw rawPeerLogEntry 18 | if err := json.Unmarshal(data, &raw); err != nil { 19 | return err 20 | } 21 | t := time.Unix(0, int64(raw.Timestamp)*int64(time.Millisecond)) 22 | *l = PeerLogEntry{ 23 | ID: raw.ID, 24 | IP: raw.IP, 25 | Timestamp: t, 26 | Blocked: raw.Blocked, 27 | Reason: raw.Reason, 28 | } 29 | return nil 30 | } 31 | 32 | type rawPeerLogEntry struct { 33 | ID int `json:"id"` 34 | IP string `json:"ip"` 35 | Timestamp int `json:"timestamp"` 36 | Blocked bool `json:"blocked"` 37 | Reason string `json:"reason"` 38 | } 39 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/log_entry.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type LogEntry struct { 9 | ID int `json:"id"` 10 | Message string `json:"message"` 11 | Timestamp time.Time `json:"timestamp"` 12 | Type LogType `json:"type"` 13 | } 14 | 15 | func (l *LogEntry) UnmarshalJSON(data []byte) error { 16 | var raw rawLogEntry 17 | if err := json.Unmarshal(data, &raw); err != nil { 18 | return err 19 | } 20 | t := time.Unix(0, int64(raw.Timestamp)*int64(time.Millisecond)) 21 | *l = LogEntry{ 22 | ID: raw.ID, 23 | Message: raw.Message, 24 | Timestamp: t, 25 | Type: raw.Type, 26 | } 27 | return nil 28 | } 29 | 30 | type LogType int 31 | 32 | const ( 33 | TypeNormal LogType = iota << 1 34 | TypeInfo 35 | TypeWarning 36 | TypeCritical 37 | ) 38 | 39 | type rawLogEntry struct { 40 | ID int `json:"id"` 41 | Message string `json:"message"` 42 | Timestamp int `json:"timestamp"` 43 | Type LogType `json:"type"` 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | goos: [linux] 14 | goarch: [amd64] 15 | fail-fast: false 16 | 17 | runs-on: ubuntu-latest 18 | env: 19 | GOOS: ${{ matrix.goos }} 20 | GOARCH: ${{ matrix.goarch }} 21 | GOARM: ${{ matrix.goarm }} 22 | CGO_ENABLED: 0 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v2 28 | with: 29 | go-version: 1.15 30 | 31 | - name: Build Main 32 | run: go build -o goseeder main.go 33 | 34 | - name: Build Stat Tool 35 | run: go build -o gostat stat.go 36 | 37 | - name: Upload a Build Artifact 38 | uses: actions/upload-artifact@v2.2.3 39 | with: 40 | name: my-artifact 41 | path: | 42 | goseeder 43 | gostat 44 | 45 | -------------------------------------------------------------------------------- /src/nexus/client.go: -------------------------------------------------------------------------------- 1 | package nexus 2 | 3 | import ( 4 | "github.com/mmcdole/gofeed" 5 | "seeder/src/config" 6 | "strconv" 7 | ) 8 | 9 | type Client struct { 10 | baseURL string 11 | Rule config.NodeRule 12 | } 13 | 14 | type Torrent struct { 15 | GUID string 16 | Title string 17 | URL string 18 | Size string 19 | } 20 | 21 | func NewClient(source string, limit int, passkey string,Rule config.NodeRule) Client { 22 | var baseURL = "https://" + source + "/torrentrss.php?rows=" + strconv.Itoa(limit) + "&linktype=dl&passkey=" + passkey 23 | return Client{ 24 | baseURL: baseURL, 25 | Rule:Rule, 26 | } 27 | } 28 | 29 | func (c *Client) Get() ([]Torrent, error) { 30 | var ts []Torrent 31 | fp := gofeed.NewParser() 32 | feed, err := fp.ParseURL(c.baseURL) 33 | if err == nil { 34 | for _, value := range feed.Items { 35 | ts = append(ts, Torrent{ 36 | GUID: value.GUID, 37 | Title: value.Title, 38 | URL: value.Enclosures[0].URL, 39 | Size: value.Enclosures[0].Length, 40 | }) 41 | } 42 | return ts, nil 43 | } 44 | 45 | return nil, err 46 | } 47 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/rule_definition.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type RuleDefinition struct { 4 | // Whether the rule is enabled 5 | Enabled bool `json:"enabled"` 6 | // The substring that the torrent name must contain 7 | MustContain string `json:"mustContain"` 8 | // The substring that the torrent name must not contain 9 | MustNotContain string `json:"mustNotContain"` 10 | // Enable regex mode in "mustContain" and "mustNotContain" 11 | UseRegex bool `json:"useRegex"` 12 | // Episode filter definition 13 | EpisodeFilter string `json:"episodeFilter"` 14 | // Enable smart episode filter 15 | SmartFilter bool `json:"smartFilter"` 16 | // The list of episode IDs already matched by smart filter 17 | PreviouslyMatchedEpisodes []string `json:"previouslyMatchedEpisodes"` 18 | // The feed URLs the rule applied to 19 | AffectedFeeds []string `json:"affectedFeeds"` 20 | // Ignore sunsequent rule matches 21 | IgnoreDays int `json:"ignoreDays"` 22 | // The rule last match time 23 | LastMatch string `json:"lastMatch"` 24 | // Add matched torrent in paused mode 25 | AddPaused bool `json:"addPaused"` 26 | // Assign category to the torrent 27 | AssignedCategory string `json:"assignedCategory"` 28 | // Save torrent to the given directory 29 | SavePath string `json:"savePath"` 30 | } 31 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/add_torrents_options.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AddTorrentsOptions struct { 4 | // Download folder 5 | Savepath string `json:"savepath,omitempty"` 6 | // Cookie sent to download the .torrent file 7 | Cookie string `json:"cookie,omitempty"` 8 | // Category for the torrent 9 | Category string `json:"category,omitempty"` 10 | // Category for the torrent 11 | Tags string `json:"tags,omitempty"` 12 | // Skip hash checking. 13 | SkipChecking bool `json:"skip_checking,omitempty"` 14 | // Add torrents in the paused state. 15 | Paused string `json:"paused,omitempty"` 16 | // Create the root folder. Possible values are true, false, unset (default) 17 | RootFolder string `json:"root_folder,omitempty"` 18 | // Rename torrent 19 | Rename string `json:"rename,omitempty"` 20 | // Set torrent upload speed limit. Unit in bytes/second 21 | UpLimit string `json:"upLimit,omitempty"` 22 | // Set torrent download speed limit. Unit in bytes/second 23 | DlLimit string `json:"dlLimit,omitempty"` 24 | // Whether Automatic Torrent Management should be used 25 | UseAutoTMM bool `json:"useAutoTMM,omitempty"` 26 | // Enable sequential download. Possible values are true, false (default) 27 | SequentialDownload bool `json:"sequentialDownload,omitempty"` 28 | // Prioritize download first last piece. Possible values are true, false (default) 29 | FirstLastPiecePrio bool `json:"firstLastPiecePrio,omitempty"` 30 | } 31 | -------------------------------------------------------------------------------- /src/datebase/client.go: -------------------------------------------------------------------------------- 1 | package datebase 2 | 3 | import ( 4 | "database/sql" 5 | _ "github.com/mattn/go-sqlite3" 6 | "time" 7 | ) 8 | 9 | type Client struct { 10 | DB *sql.DB 11 | } 12 | 13 | func NewClient() Client { 14 | db, err := sql.Open("sqlite3", "/usr/local/goseeder.db") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS Torrent (torrent_hash CHAR,title CHAR,torrent_announce CHAR,create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY('torrent_hash'))") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | return Client{ 25 | DB: db, 26 | } 27 | } 28 | 29 | func (c *Client) Get(hashId string) bool { 30 | var torrent_hash string 31 | var title string 32 | var torrent_announce string 33 | var create_time time.Time 34 | 35 | // 查询数据 36 | rows, err := c.DB.Query("SELECT * FROM Torrent WHERE torrent_hash == '" + hashId + "'") 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | defer rows.Close() 42 | rows.Next() 43 | 44 | err = rows.Scan(&torrent_hash, &title, &torrent_announce, &create_time) 45 | if err == nil { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | func (c *Client) Insert(Title string, TorrentHash string, TorrentAnnounce string) bool { 53 | // 插入数据 54 | stmt, err := c.DB.Prepare("INSERT INTO Torrent ('torrent_hash', 'title', 'torrent_announce') VALUES (?,?,?)") 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | _, err = stmt.Exec(TorrentHash, Title, TorrentAnnounce) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/robfig/cron" 6 | "os" 7 | "seeder/src/config" 8 | "seeder/src/datebase" 9 | "seeder/src/nexus" 10 | "seeder/src/qbittorrent" 11 | "strconv" 12 | ) 13 | 14 | func main() { 15 | var db datebase.Client 16 | var nodes []nexus.Client 17 | var servers []qbittorrent.Server 18 | 19 | cron := cron.New() 20 | if cfg, err := config.GetConfig(); err == nil { 21 | db = datebase.NewClient() 22 | for _, value := range cfg.Node { 23 | if value.Enable == true { 24 | node := nexus.NewClient(value.Source, value.Limit, value.Passkey, value.Rule) 25 | nodes = append(nodes, node) 26 | } 27 | } 28 | for _, value := range cfg.Server { 29 | if value.Enable == true { 30 | server := qbittorrent.NewClientWrapper(value.Endpoint, value.Username, value.Password, value.Remark, value.Rule) 31 | 32 | server.CalcEstimatedQuota() 33 | server.ServerClean(cfg) 34 | 35 | cron.AddFunc("@every 5s", func() { server.CalcEstimatedQuota() }) 36 | cron.AddFunc("@every 1m", func() { server.AnnounceRace() }) 37 | cron.AddFunc("@every 1m", func() { server.ServerClean(cfg) }) 38 | cron.Start() 39 | 40 | servers = append(servers, server) 41 | } 42 | } 43 | } else { 44 | os.Exit(1) 45 | } 46 | 47 | for true { 48 | var ts []nexus.Torrent 49 | for _, node := range nodes { 50 | ts, _ = node.Get() 51 | for _, t := range ts { 52 | // 解决重复添加问题 53 | for _, server := range servers { 54 | server.CalcEstimatedQuota() 55 | if db.Get(t.GUID) == false { 56 | if Size, err := strconv.Atoi(t.Size); err == nil { 57 | if server.AddTorrentByURL(t.URL, Size, int(node.Rule.SpeedLimit*1024*1024)) == true { 58 | fmt.Println("[添加]种子:" + t.Title) 59 | db.Insert(t.Title, t.GUID, t.URL) 60 | } 61 | } 62 | } 63 | } 64 | if db.Get(t.GUID) == false { 65 | //找遍了所有服务器(10次尝试),还是没法找到添加的,那么,就直接插入数据库不再尝试. 66 | fmt.Println("[忽略]种子:" + t.Title) 67 | db.Insert(t.Title, t.GUID, t.URL) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tomcraven/gotable" 6 | "os" 7 | "seeder/src/config" 8 | "seeder/src/qbittorrent" 9 | "seeder/src/qbittorrent/pkg/model" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | var servers []qbittorrent.Server 15 | if cfg, err := config.GetConfig(); err == nil { 16 | for _, value := range cfg.Server { 17 | if value.Enable == true { 18 | server := qbittorrent.NewClientWrapper(value.Endpoint, value.Username, value.Password, value.Remark, value.Rule) 19 | servers = append(servers, server) 20 | } 21 | } 22 | } else { 23 | os.Exit(1) 24 | } 25 | 26 | for true { 27 | t := gotable.NewTable([]gotable.Column{ 28 | gotable.NewColumn("Name", 20), 29 | gotable.NewColumn("FreeDisk(GB)", 20), 30 | gotable.NewColumn("DiskLatency(ms)", 20), 31 | gotable.NewColumn("CurrentSpeed(MB/s)", 20), 32 | gotable.NewColumn("TaskList(Count)", 20), 33 | gotable.NewColumn("Transfer(TB)", 20), 34 | gotable.NewColumn("Ratio(%)", 20), 35 | }) 36 | 37 | for _, server := range servers { 38 | if r, err := server.Client.GetMainData(); err == nil { 39 | ConcurrentDownload := 0 40 | ConcurrentUpload := 0 41 | TaskCount := 0 42 | 43 | var options model.GetTorrentListOptions 44 | options.Filter = "all" 45 | if ts, err := server.Client.GetList(); err == nil { 46 | for _, t := range ts { 47 | if t.AmountLeft != 0 { 48 | ConcurrentDownload++ 49 | } 50 | if t.Upspeed > 0 { 51 | ConcurrentUpload++ 52 | } 53 | TaskCount++ 54 | server.Status.EstimatedQuota -= t.AmountLeft 55 | } 56 | } else { 57 | //如果无法获取状态,直接让并行任务数显示最大以跳过规则. 58 | server.Status.ConcurrentDownload = 65535 59 | } 60 | 61 | t.Push( 62 | server.Remark, 63 | fmt.Sprintf("%.2f", float64(r.ServerState.FreeSpaceOnDisk)/1073741820), 64 | fmt.Sprintf("%d", r.ServerState.AverageTimeQueue), 65 | fmt.Sprintf("%.2f(U)|%.2f(D)", float64(r.ServerState.UpInfoSpeed)/1048576.0, float64(r.ServerState.DlInfoSpeed)/1048576.0), 66 | fmt.Sprintf("%d(U)|%d(D)|%d(A)", ConcurrentUpload, ConcurrentDownload, TaskCount), 67 | fmt.Sprintf("%.2f(U)|%.2f(D)", float64(r.ServerState.AlltimeUl)/1099511623680, float64(r.ServerState.AlltimeDl)/1099511623680), 68 | fmt.Sprintf("%.2f", float64(r.ServerState.GlobalRatio)), 69 | ) 70 | } 71 | } 72 | 73 | fmt.Printf("\x1bc") 74 | fmt.Println("QB服务器最新状态:") 75 | t.Print() 76 | 77 | time.Sleep(5) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbserver": "mongodb+srv://username:password@cluster0.3cyul.mongodb.net/?retryWrites=true&w=majority", // (请在正式配置中不要包含此注释) => 数据库 3 | "node": [ 4 | { 5 | "source": "www.nicept.net", 6 | "passkey": "21232f297a57a5a743894a0e4a801fc3", 7 | "limit": 10, 8 | "enable": true, 9 | "rule": { 10 | "seeder_time": 0, 11 | "seeder_ratio": 0, 12 | "speed_limit": 8.0 13 | } 14 | }, 15 | { 16 | "source": "pt.btschool.club", // (请在正式配置中不要包含此注释) => 网站域名 17 | "passkey": "21232f297a57a5a743894a0e4a801fc3", // (请在正式配置中不要包含此注释) => 密钥 18 | "limit": 10, // (请在正式配置中不要包含此注释) => 每次抓取数量,过多可能会D挂PT站 19 | "enable": true, // (请在正式配置中不要包含此注释) => 如果为false,则这个配置暂时不用 20 | "rule": { 21 | "seeder_time": 86400, // (请在正式配置中不要包含此注释) => 该站有HR,限制最小做种86400秒 22 | "seeder_ratio": 1.2, // (请在正式配置中不要包含此注释) => 该站有HR,限制分享率1.2 23 | "speed_limit": 10.0 // (请在正式配置中不要包含此注释) => 该站限速10.0MB/s 24 | } 25 | } 26 | ], 27 | "server": [ 28 | { 29 | "endpoint": "http://173.82.120.1:8080", // (请在正式配置中不要包含此注释) => QB服务器地址 30 | "username": "admin", // (请在正式配置中不要包含此注释) => QB用户名 31 | "password": "adminadmin", // (请在正式配置中不要包含此注释) => QB密码 32 | "remark": "Cloudcone (250G)", // (请在正式配置中不要包含此注释) => 服务器别名 33 | "enable": false, // (请在正式配置中不要包含此注释) => 如果false则不使能 34 | "rule": { 35 | "concurrent_download": 1, // (请在正式配置中不要包含此注释) => 同时下载限制:1个 36 | "disk_threshold": 10.0, // (请在正式配置中不要包含此注释) => 10GB 37 | "disk_overcommit": true, // (请在正式配置中不要包含此注释) => 是否可超量安排任务(如果盘小推荐关闭) 38 | "max_speed": 50.00, // (请在正式配置中不要包含此注释) => 当服务器当前速度大于50MB/s时不再安排任务,避免TOS. 39 | "min_alivetime": 3600, // (请在正式配置中不要包含此注释) => 种子在服务器最短存活时间3600秒,小于这个时间即使空间不足也不会删除. 40 | "max_alivetime": 86400, // (请在正式配置中不要包含此注释) => 种子在服务器最长存货时间86400秒,大于这个时间的不活跃内容会被自动删除. 41 | "min_tasksize": 1.0, // (请在正式配置中不要包含此注释) => 添加的最小种子1GB,太小的话IO太密集. 42 | "max_tasksize": 200.0,// (请在正式配置中不要包含此注释) => 添加的最大种子200GB,太大话刷的效率低. 43 | "max_disklatency": 10000 // (请在正式配置中不要包含此注释) => 磁盘延迟大于10000就不要添加种子,添进去怕也下不动.. 44 | } 45 | }, 46 | { 47 | "endpoint": "http://173.82.120.2:8080", 48 | "username": "admin", 49 | "password": "TaterLi1024", 50 | "remark": "Cloudcone (500G)", 51 | "enable": false, 52 | "rule": { 53 | "concurrent_download": 1, 54 | "disk_threshold": 10.0, 55 | "disk_overcommit": true, 56 | "max_speed": 50.00, 57 | "min_alivetime": 3600, 58 | "max_alivetime": 86400, 59 | "min_tasksize": 0.0, 60 | "max_tasksize": 400.0, 61 | "max_disklatency": 10000 62 | } 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/server_state.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | ) 7 | 8 | type ServerState struct { 9 | TransferInfo 10 | AlltimeDl int `json:"alltime_dl"` 11 | AlltimeUl int `json:"alltime_ul"` 12 | AverageTimeQueue int `json:"average_time_queue"` 13 | FreeSpaceOnDisk int `json:"free_space_on_disk"` 14 | GlobalRatio float64 `json:"global_ratio"` 15 | QueuedIoJobs int `json:"queued_io_jobs"` 16 | ReadCacheHits float64 `json:"read_cache_hits"` 17 | ReadCacheOverload float64 `json:"read_cache_overload"` 18 | TotalBuffersSize int `json:"total_buffers_size"` 19 | TotalPeerConnections int `json:"total_peer_connections"` 20 | TotalQueuedSize int `json:"total_queued_size"` 21 | TotalWastedSession int `json:"total_wasted_session"` 22 | WriteCacheOverload float64 `json:"write_cache_overload"` 23 | } 24 | 25 | func (s *ServerState) UnmarshalJSON(data []byte) error { 26 | var raw rawServerState 27 | if err := json.Unmarshal(data, &raw); err != nil { 28 | return err 29 | } 30 | globalRatio, err := strconv.ParseFloat(raw.GlobalRatio, 64) 31 | if err != nil { 32 | return err 33 | } 34 | readCacheHits, err := strconv.ParseFloat(raw.ReadCacheHits, 64) 35 | if err != nil { 36 | return err 37 | } 38 | readCacheOverload, err := strconv.ParseFloat(raw.ReadCacheOverload, 64) 39 | if err != nil { 40 | return err 41 | } 42 | writeCacheOverload, err := strconv.ParseFloat(raw.WriteCacheOverload, 64) 43 | if err != nil { 44 | return err 45 | } 46 | *s = ServerState{ 47 | TransferInfo: raw.TransferInfo, 48 | AlltimeDl: raw.AlltimeDl, 49 | AlltimeUl: raw.AlltimeUl, 50 | AverageTimeQueue: raw.AverageTimeQueue, 51 | FreeSpaceOnDisk: raw.FreeSpaceOnDisk, 52 | GlobalRatio: globalRatio, 53 | QueuedIoJobs: raw.QueuedIoJobs, 54 | ReadCacheHits: readCacheHits, 55 | ReadCacheOverload: readCacheOverload, 56 | TotalBuffersSize: raw.TotalBuffersSize, 57 | TotalPeerConnections: raw.TotalPeerConnections, 58 | TotalQueuedSize: raw.TotalQueuedSize, 59 | TotalWastedSession: raw.TotalWastedSession, 60 | WriteCacheOverload: writeCacheOverload, 61 | } 62 | return nil 63 | } 64 | 65 | type rawServerState struct { 66 | TransferInfo 67 | AlltimeDl int `json:"alltime_dl"` 68 | AlltimeUl int `json:"alltime_ul"` 69 | AverageTimeQueue int `json:"average_time_queue"` 70 | FreeSpaceOnDisk int `json:"free_space_on_disk"` 71 | GlobalRatio string `json:"global_ratio"` 72 | QueuedIoJobs int `json:"queued_io_jobs"` 73 | ReadCacheHits string `json:"read_cache_hits"` 74 | ReadCacheOverload string `json:"read_cache_overload"` 75 | TotalBuffersSize int `json:"total_buffers_size"` 76 | TotalPeerConnections int `json:"total_peer_connections"` 77 | TotalQueuedSize int `json:"total_queued_size"` 78 | TotalWastedSession int `json:"total_wasted_session"` 79 | WriteCacheOverload string `json:"write_cache_overload"` 80 | } 81 | -------------------------------------------------------------------------------- /src/config/client.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type NodeRule struct { 12 | SeederTime int `json:"seeder_time"` 13 | SeederRatio float64 `json:"seeder_ratio"` // Unit:MB 14 | SpeedLimit float64 `json:"speed_limit"` // Unit:MB 15 | } 16 | 17 | type Node struct { 18 | Source string `json:"source"` 19 | Passkey string `json:"passkey"` 20 | Limit int `json:"limit"` 21 | Enable bool `json:"enable"` 22 | Rule NodeRule `json:"rule"` 23 | } 24 | 25 | type ServerRule struct { 26 | ConcurrentDownload int `json:"concurrent_download"` 27 | DiskThreshold float64 `json:"disk_threshold"` // Unit:GB 28 | DiskOverCommit bool `json:"disk_overcommit"` 29 | MaxSpeed float64 `json:"max_speed"` // Unit:MB 30 | MinAliveTime int `json:"min_alivetime"` // Unit:s 31 | MaxAliveTime int `json:"max_alivetime"` // Unit:s 32 | MinTaskSize float64 `json:"min_tasksize"` // Unit:GB 33 | MaxTaskSize float64 `json:"max_tasksize"` // Unit:GB 34 | MaxDiskLatency int `json:"max_disklatency"` // Unit:ms 35 | } 36 | 37 | type RawServerRule struct { 38 | ConcurrentDownload int `json:"concurrent_download"` 39 | DiskThreshold int `json:"disk_threshold"` 40 | DiskOverCommit bool `json:"disk_overcommit"` 41 | MaxSpeed int `json:"max_speed"` 42 | MinAliveTime int `json:"min_alivetime"` 43 | MaxAliveTime int `json:"max_alivetime"` 44 | MinTaskSize int `json:"min_tasksize"` 45 | MaxTaskSize int `json:"max_tasksize"` 46 | MaxDiskLatency int `json:"max_disklatency"` 47 | } 48 | 49 | type Server struct { 50 | Endpoint string `json:"endpoint"` 51 | Username string `json:"username"` 52 | Password string `json:"password"` 53 | Remark string `json:"remark"` 54 | Enable bool `json:"enable"` 55 | Rule ServerRule `json:"rule"` 56 | } 57 | 58 | type Config struct { 59 | // DB Confoig 60 | Db string `json:"dbserver"` 61 | // PT Datasource 62 | Node []Node `json:"node"` 63 | // QB Server 64 | Server []Server `json:"server"` 65 | } 66 | 67 | func GetConfig() (Config, error) { 68 | var cfg Config 69 | configFile := GetConfigFilePath() 70 | // Open our jsonFile 71 | jsonFile, err := os.Open(configFile) 72 | // if we os.Open returns an error then handle it 73 | if err != nil { 74 | return cfg, errors.New("failed to open config!") 75 | } 76 | 77 | // defer the closing of our jsonFile so that we can parse it later on 78 | defer jsonFile.Close() 79 | 80 | byteValue, _ := ioutil.ReadAll(jsonFile) 81 | 82 | err = json.Unmarshal(byteValue, &cfg) 83 | if err != nil { 84 | return cfg, err 85 | } 86 | return cfg, err 87 | } 88 | 89 | func GetConfigFilePath() string { 90 | if workingDir, err := os.Getwd(); err == nil { 91 | configFile := filepath.Join(workingDir, "config.json") 92 | if fileExists(configFile) { 93 | return configFile 94 | } 95 | } 96 | 97 | if fileExists("/etc/goseeder.conf") { 98 | return "/etc/goseeder.conf" 99 | } 100 | 101 | return "" 102 | } 103 | 104 | func fileExists(file string) bool { 105 | info, err := os.Stat(file) 106 | return err == nil && !info.IsDir() 107 | } 108 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/torrent.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Torrent struct { 4 | // Completion On Timestamp 5 | CompletionOn int `json:"completion_on"` 6 | // Added On Timestamp 7 | AddedOn int `json:"added_on"` 8 | // Amount left 9 | AmountLeft int `json:"amount_left"` 10 | // Torrent hash 11 | Hash string `json:"hash"` 12 | // Torrent name 13 | Name string `json:"name"` 14 | // Total size (bytes) of files selected for download 15 | Size int `json:"size"` 16 | // Torrent progress (percentage/100) 17 | Progress float64 `json:"progress"` 18 | // Torrent download speed (bytes/s) 19 | Dlspeed int `json:"dlspeed"` 20 | // Torrent upload speed (bytes/s) 21 | Upspeed int `json:"upspeed"` 22 | // Torrent priority. Returns -1 if queuing is disabled or torrent is in seed mode 23 | Priority int `json:"priority"` 24 | // Number of seeds connected to 25 | NumSeeds int `json:"num_seeds"` 26 | // Number of seeds in the swarm 27 | NumComplete int `json:"num_complete"` 28 | // Number of leechers connected to 29 | NumLeechs int `json:"num_leechs"` 30 | // Number of leechers in the swarm 31 | NumIncomplete int `json:"num_incomplete"` 32 | // Torrent share ratio. Max ratio value: 9999. 33 | Ratio float64 `json:"ratio"` 34 | // Torrent ETA (seconds) 35 | Eta int `json:"eta"` 36 | // Torrent state. See table here below for the possible values 37 | State TorrentState `json:"state"` 38 | // True if sequential download is enabled 39 | SeqDl bool `json:"seq_dl"` 40 | // True if first last piece are prioritized 41 | FLPiecePrio bool `json:"f_l_piece_prio"` 42 | // Category of the torrent 43 | Category string `json:"category"` 44 | // True if super seeding is enabled 45 | SuperSeeding bool `json:"super_seeding"` 46 | // True if force start is enabled for this torrent 47 | ForceStart bool `json:"force_start"` 48 | } 49 | 50 | type TorrentState string 51 | 52 | const ( 53 | // Some error occurred, applies to paused torrents 54 | StateError TorrentState = "error" 55 | // Torrent data files is missing 56 | StateMissingFiles TorrentState = "missingFiles" 57 | // Torrent is being seeded and data is being transferred 58 | StateUploading TorrentState = "uploading" 59 | // Torrent is paused and has finished downloading 60 | StatePausedUP TorrentState = "pausedUP" 61 | // Queuing is enabled and torrent is queued for upload 62 | StateQueuedUP TorrentState = "queuedUP" 63 | // Torrent is being seeded, but no connection were made 64 | StateStalledUP TorrentState = "stalledUP" 65 | // Torrent has finished downloading and is being checked 66 | StateCheckingUP TorrentState = "checkingUP" 67 | // Torrent is forced to uploading and ignore queue limit 68 | StateForcedUP TorrentState = "forcedUP" 69 | // Torrent is allocating disk space for download 70 | StateAllocating TorrentState = "allocating" 71 | // Torrent is being downloaded and data is being transferred 72 | StateDownloading TorrentState = "downloading" 73 | // Torrent has just started downloading and is fetching metadata 74 | StateMetaDL TorrentState = "metaDL" 75 | // Torrent is paused and has NOT finished downloading 76 | StatePausedDL TorrentState = "pausedDL" 77 | // Queuing is enabled and torrent is queued for download 78 | StateQueuedDL TorrentState = "queuedDL" 79 | // Torrent is being downloaded, but no connection were made 80 | StateStalledDL TorrentState = "stalledDL" 81 | // Same as checkingUP, but torrent has NOT finished downloading 82 | StateCheckingDL TorrentState = "checkingDL" 83 | // Torrent is forced to downloading to ignore queue limit 84 | StateForceDL TorrentState = "forceDL" 85 | // Checking resume data on qBt startup 86 | StateCheckingResumeData TorrentState = "checkingResumeData" 87 | // Torrent is moving to another location 88 | StateMoving TorrentState = "moving" 89 | // Unknown status 90 | StateUnknown TorrentState = "unknown" 91 | ) 92 | -------------------------------------------------------------------------------- /src/qbittorrent/client.go: -------------------------------------------------------------------------------- 1 | package qbittorrent 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "golang.org/x/net/publicsuffix" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "net/http/cookiejar" 13 | "net/url" 14 | "seeder/src/qbittorrent/pkg/model" 15 | "strconv" 16 | "strings" 17 | 18 | ) 19 | 20 | type Client struct { 21 | baseURL string 22 | loginURI string 23 | client *http.Client 24 | } 25 | 26 | 27 | func Auth(baseURL string, loginURI string) (*http.Client,error) { 28 | client := &http.Client {} 29 | req, err := http.NewRequest("GET", loginURI, nil) 30 | 31 | if err != nil { 32 | fmt.Println(err) 33 | return nil,err 34 | } 35 | 36 | res, err := client.Do(req) 37 | if err != nil { 38 | fmt.Println(err) 39 | return nil,err 40 | } 41 | 42 | defer res.Body.Close() 43 | 44 | body, err := ioutil.ReadAll(res.Body) 45 | if err != nil { 46 | fmt.Println(err) 47 | return nil,err 48 | } 49 | 50 | if string(body) != "Ok." { 51 | return nil,errors.New("Password Error!") 52 | } 53 | 54 | jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) 55 | if err != nil { 56 | return nil,err 57 | } 58 | apiURL, err := url.Parse(baseURL) 59 | jar.SetCookies(apiURL, []*http.Cookie{res.Cookies()[0]}) 60 | client.Jar = jar 61 | 62 | return client,err 63 | } 64 | 65 | func (c *Client) GetInto(url string, target interface{}) (err error) { 66 | req, err := http.NewRequest("GET", c.baseURL + url, nil) 67 | 68 | if err != nil { 69 | Auth(c.baseURL,c.loginURI) 70 | return err 71 | } 72 | 73 | res, err := c.client.Do(req) 74 | if err != nil { 75 | Auth(c.baseURL,c.loginURI) 76 | return err 77 | } 78 | 79 | defer res.Body.Close() 80 | 81 | body, err := ioutil.ReadAll(res.Body) 82 | if err != nil { 83 | Auth(c.baseURL,c.loginURI) 84 | return err 85 | } 86 | 87 | if err := json.NewDecoder(bytes.NewReader(body)).Decode(target); err != nil { 88 | if err2 := json.NewDecoder(strings.NewReader(`"` + string(body) + `"`)).Decode(target); err2 != nil { 89 | Auth(c.baseURL,c.loginURI) 90 | return err 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (c Client) GetMainData() (*model.SyncMainData, error) { 98 | var res model.SyncMainData 99 | 100 | err := c.GetInto("/sync/maindata",&res) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return &res, nil 105 | } 106 | 107 | 108 | func (c Client) GetList() ([]*model.Torrent, error) { 109 | var res []*model.Torrent 110 | err := c.GetInto("/torrents/info?filter=all",&res) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return res, nil 115 | } 116 | 117 | func (c Client) GetTransferInfo() (*model.TransferInfo, error) { 118 | var res model.TransferInfo 119 | if err := c.GetInto("/transfer/info",&res) ;err != nil { 120 | return nil, err 121 | } 122 | return &res, nil 123 | } 124 | 125 | func (c Client) GetTrackers(hash string) ([]*model.TorrentTracker, error) { 126 | var res []*model.TorrentTracker 127 | if err := c.GetInto("/torrents/trackers?hash=" + hash,&res); err != nil { 128 | return nil, err 129 | } 130 | return res, nil 131 | } 132 | 133 | func (c Client) DeleteTorrents(hash string) error { 134 | var res string 135 | if err := c.GetInto("/torrents/delete?hashes=" + hash + "&deleteFiles=true",&res); err != nil { 136 | return err 137 | } 138 | return nil 139 | } 140 | 141 | func (c Client) ReannounceTorrents(hash string) error { 142 | var res string 143 | if err := c.GetInto("/torrents/reannounce?hashes=" + hash,&res); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func (c Client) SetDownloadLimit(limit int) error { 150 | var res string 151 | if err := c.GetInto("/transfer/setDownloadLimit?limit=" + strconv.Itoa(limit),&res); err != nil { 152 | return err 153 | } 154 | return nil 155 | } 156 | 157 | func (c Client) AddURLs(DestLink string,options *model.AddTorrentsOptions) error { 158 | payload := &bytes.Buffer{} 159 | writer := multipart.NewWriter(payload) 160 | _ = writer.WriteField("urls", DestLink) 161 | _ = writer.WriteField("category", options.Category) 162 | _ = writer.WriteField("savepath", options.Savepath) 163 | _ = writer.WriteField("upLimit", options.UpLimit) 164 | _ = writer.WriteField("dlLimit", options.DlLimit) 165 | err := writer.Close() 166 | if err != nil { 167 | fmt.Println(err) 168 | return err 169 | } 170 | 171 | req, err := http.NewRequest("POST", c.baseURL + "/torrents/add", payload) 172 | 173 | if err != nil { 174 | fmt.Println(err) 175 | return err 176 | } 177 | 178 | req.Header.Set("Content-Type", writer.FormDataContentType()) 179 | res, err := c.client.Do(req) 180 | if err != nil { 181 | fmt.Println(err) 182 | return err 183 | } 184 | defer res.Body.Close() 185 | 186 | body, err := ioutil.ReadAll(res.Body) 187 | if err != nil { 188 | fmt.Println(err) 189 | return err 190 | } 191 | 192 | if string(body) != "Ok." { 193 | return errors.New("AddURL Error!") 194 | } 195 | 196 | return nil 197 | } 198 | 199 | func NewClient(baseURL string,username string,password string) (*Client,error) { 200 | baseURL = baseURL + "/api/v2" 201 | loginURI := baseURL + "/auth/login?username=" + username + "&password=" + password 202 | client, err := Auth(baseURL,loginURI) 203 | 204 | c := Client{ 205 | baseURL: baseURL, 206 | loginURI: loginURI, 207 | client: client, 208 | } 209 | 210 | 211 | return &c, err 212 | } 213 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/torrent_properties.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type TorrentProperties struct { 9 | // Torrent save path 10 | SavePath string `json:"save_path"` 11 | // Torrent creation date (Unix timestamp) 12 | CreationDate time.Time `json:"creation_date"` 13 | // Torrent piece size (bytes) 14 | PieceSize int `json:"piece_size"` 15 | // Torrent comment 16 | Comment string `json:"comment"` 17 | // Total data wasted for torrent (bytes) 18 | TotalWasted int `json:"total_wasted"` 19 | // Total data uploaded for torrent (bytes) 20 | TotalUploaded int `json:"total_uploaded"` 21 | // Total data uploaded this session (bytes) 22 | TotalUploadedSession int `json:"total_uploaded_session"` 23 | // Total data downloaded for torrent (bytes) 24 | TotalDownloaded int `json:"total_downloaded"` 25 | // Total data downloaded this session (bytes) 26 | TotalDownloadedSession int `json:"total_downloaded_session"` 27 | // Torrent upload limit (bytes/s) 28 | UpLimit int `json:"up_limit"` 29 | // Torrent download limit (bytes/s) 30 | DlLimit int `json:"dl_limit"` 31 | // Torrent elapsed time (seconds) 32 | TimeElapsed int `json:"time_elapsed"` 33 | // Torrent elapsed time while complete (seconds) 34 | SeedingTime time.Duration `json:"seeding_time"` 35 | // Torrent connection count 36 | NbConnections int `json:"nb_connections"` 37 | // Torrent connection count limit 38 | NbConnectionsLimit int `json:"nb_connections_limit"` 39 | // Torrent share ratio 40 | ShareRatio float64 `json:"share_ratio"` 41 | // When this torrent was added (unix timestamp) 42 | AdditionDate time.Time `json:"addition_date"` 43 | // Torrent completion date (unix timestamp) 44 | CompletionDate time.Time `json:"completion_date"` 45 | // Torrent creator 46 | CreatedBy string `json:"created_by"` 47 | // Torrent average download speed (bytes/second) 48 | DlSpeedAvg int `json:"dl_speed_avg"` 49 | // Torrent download speed (bytes/second) 50 | DlSpeed int `json:"dl_speed"` 51 | // Torrent ETA (seconds) 52 | Eta time.Duration `json:"eta"` 53 | // Last seen complete date (unix timestamp) 54 | LastSeen time.Time `json:"last_seen"` 55 | // Number of peers connected to 56 | Peers int `json:"peers"` 57 | // Number of peers in the swarm 58 | PeersTotal int `json:"peers_total"` 59 | // Number of pieces owned 60 | PiecesHave int `json:"pieces_have"` 61 | // Number of pieces of the torrent 62 | PiecesNum int `json:"pieces_num"` 63 | // Number of seconds until the next announce 64 | Reannounce time.Duration `json:"reannounce"` 65 | // Number of seeds connected to 66 | Seeds int `json:"seeds"` 67 | // Number of seeds in the swarm 68 | SeedsTotal int `json:"seeds_total"` 69 | // Torrent total size (bytes) 70 | TotalSize int `json:"total_size"` 71 | // Torrent average upload speed (bytes/second) 72 | UpSpeedAvg int `json:"up_speed_avg"` 73 | // Torrent upload speed (bytes/second) 74 | UpSpeed int `json:"up_speed"` 75 | } 76 | 77 | func (p *TorrentProperties) UnmarshalJSON(data []byte) error { 78 | var raw rawTorrentProperties 79 | if err := json.Unmarshal(data, &raw); err != nil { 80 | return err 81 | } 82 | creationDate := time.Unix(int64(raw.CreationDate), 0) 83 | seedingTime := time.Duration(raw.SeedingTime) * time.Second 84 | additionDate := time.Unix(int64(raw.AdditionDate), 0) 85 | completionDate := time.Unix(int64(raw.CompletionDate), 0) 86 | eta := time.Duration(raw.Eta) * time.Second 87 | lastSeen := time.Unix(int64(raw.LastSeen), 0) 88 | reannounce := time.Duration(raw.Reannounce) * time.Second 89 | *p = TorrentProperties{ 90 | SavePath: raw.SavePath, 91 | CreationDate: creationDate, 92 | PieceSize: raw.PieceSize, 93 | Comment: raw.Comment, 94 | TotalWasted: raw.TotalWasted, 95 | TotalUploaded: raw.TotalUploaded, 96 | TotalUploadedSession: raw.TotalUploadedSession, 97 | TotalDownloaded: raw.TotalDownloaded, 98 | TotalDownloadedSession: raw.TotalDownloadedSession, 99 | UpLimit: raw.UpLimit, 100 | DlLimit: raw.DlLimit, 101 | TimeElapsed: raw.TimeElapsed, 102 | SeedingTime: seedingTime, 103 | NbConnections: raw.NbConnections, 104 | NbConnectionsLimit: raw.NbConnectionsLimit, 105 | ShareRatio: raw.ShareRatio, 106 | AdditionDate: additionDate, 107 | CompletionDate: completionDate, 108 | CreatedBy: raw.CreatedBy, 109 | DlSpeedAvg: raw.DlSpeedAvg, 110 | DlSpeed: raw.DlSpeed, 111 | Eta: eta, 112 | LastSeen: lastSeen, 113 | Peers: raw.Peers, 114 | PeersTotal: raw.PeersTotal, 115 | PiecesHave: raw.PiecesHave, 116 | PiecesNum: raw.PiecesNum, 117 | Reannounce: reannounce, 118 | Seeds: raw.Seeds, 119 | SeedsTotal: raw.SeedsTotal, 120 | TotalSize: raw.TotalSize, 121 | UpSpeedAvg: raw.UpSpeedAvg, 122 | UpSpeed: raw.UpSpeed, 123 | } 124 | return nil 125 | } 126 | 127 | type rawTorrentProperties struct { 128 | // Torrent save path 129 | SavePath string `json:"save_path"` 130 | // Torrent creation date (Unix timestamp) 131 | CreationDate int `json:"creation_date"` 132 | // Torrent piece size (bytes) 133 | PieceSize int `json:"piece_size"` 134 | // Torrent comment 135 | Comment string `json:"comment"` 136 | // Total data wasted for torrent (bytes) 137 | TotalWasted int `json:"total_wasted"` 138 | // Total data uploaded for torrent (bytes) 139 | TotalUploaded int `json:"total_uploaded"` 140 | // Total data uploaded this session (bytes) 141 | TotalUploadedSession int `json:"total_uploaded_session"` 142 | // Total data downloaded for torrent (bytes) 143 | TotalDownloaded int `json:"total_downloaded"` 144 | // Total data downloaded this session (bytes) 145 | TotalDownloadedSession int `json:"total_downloaded_session"` 146 | // Torrent upload limit (bytes/s) 147 | UpLimit int `json:"up_limit"` 148 | // Torrent download limit (bytes/s) 149 | DlLimit int `json:"dl_limit"` 150 | // Torrent elapsed time (seconds) 151 | TimeElapsed int `json:"time_elapsed"` 152 | // Torrent elapsed time while complete (seconds) 153 | SeedingTime int `json:"seeding_time"` 154 | // Torrent connection count 155 | NbConnections int `json:"nb_connections"` 156 | // Torrent connection count limit 157 | NbConnectionsLimit int `json:"nb_connections_limit"` 158 | // Torrent share ratio 159 | ShareRatio float64 `json:"share_ratio"` 160 | // When this torrent was added (unix timestamp) 161 | AdditionDate int `json:"addition_date"` 162 | // Torrent completion date (unix timestamp) 163 | CompletionDate int `json:"completion_date"` 164 | // Torrent creator 165 | CreatedBy string `json:"created_by"` 166 | // Torrent average download speed (bytes/second) 167 | DlSpeedAvg int `json:"dl_speed_avg"` 168 | // Torrent download speed (bytes/second) 169 | DlSpeed int `json:"dl_speed"` 170 | // Torrent ETA (seconds) 171 | Eta int `json:"eta"` 172 | // Last seen complete date (unix timestamp) 173 | LastSeen int `json:"last_seen"` 174 | // Number of peers connected to 175 | Peers int `json:"peers"` 176 | // Number of peers in the swarm 177 | PeersTotal int `json:"peers_total"` 178 | // Number of pieces owned 179 | PiecesHave int `json:"pieces_have"` 180 | // Number of pieces of the torrent 181 | PiecesNum int `json:"pieces_num"` 182 | // Number of seconds until the next announce 183 | Reannounce int `json:"reannounce"` 184 | // Number of seeds connected to 185 | Seeds int `json:"seeds"` 186 | // Number of seeds in the swarm 187 | SeedsTotal int `json:"seeds_total"` 188 | // Torrent total size (bytes) 189 | TotalSize int `json:"total_size"` 190 | // Torrent average upload speed (bytes/second) 191 | UpSpeedAvg int `json:"up_speed_avg"` 192 | // Torrent upload speed (bytes/second) 193 | UpSpeed int `json:"up_speed"` 194 | } 195 | -------------------------------------------------------------------------------- /src/qbittorrent/wrapper.go: -------------------------------------------------------------------------------- 1 | package qbittorrent 2 | 3 | import ( 4 | "fmt" 5 | "seeder/src/config" 6 | "seeder/src/qbittorrent/pkg/model" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type ServerStatus struct { 13 | FreeSpaceOnDisk int 14 | EstimatedQuota int 15 | ConcurrentDownload int 16 | UpInfoSpeed int 17 | DownInfoSpeed int 18 | DiskLatency int 19 | } 20 | 21 | type Server struct { 22 | Client *Client 23 | Rule config.RawServerRule 24 | Remark string 25 | Status ServerStatus 26 | } 27 | 28 | func (s *Server) AnnounceRace() { 29 | if ts, err := s.Client.GetList(); err == nil { 30 | for _, t := range ts { 31 | s.Client.ReannounceTorrents(t.Hash) 32 | } 33 | } 34 | } 35 | 36 | func (s *Server) ServerClean(cfg config.Config) { 37 | if s.Status.FreeSpaceOnDisk < s.Rule.DiskThreshold { 38 | if ts, err := s.Client.GetList(); err == nil { 39 | for _, t := range ts { 40 | for _, n := range cfg.Node { 41 | if n.Source == t.Category { 42 | if trackers, err := s.Client.GetTrackers(t.Hash); err == nil && (int(time.Now().Unix())-t.AddedOn) > s.Rule.MinAliveTime { 43 | for _, tracker := range trackers { 44 | if tracker.Status == model.TrackerStatusNotContacted || tracker.Status == model.TrackerStatusNotWorking { 45 | s.Client.DeleteTorrents(t.Hash) 46 | fmt.Println("[" + s.Remark + "]清理无效种子." + t.Name) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | //开始执行删除操作(第二圈,删除其中一个最古老的完成的任务.) 56 | MaxAliveTime := 0 57 | MaxAliveSeeder := "" 58 | MaxAliveName := "" 59 | 60 | if ts, err := s.Client.GetList(); err == nil { 61 | for _, t := range ts { 62 | for _, n := range cfg.Node { 63 | if n.Source == t.Category { 64 | if t.AmountLeft == 0 { 65 | if (int(time.Now().Unix()) - t.CompletionOn) > s.Rule.MaxAliveTime { 66 | if MaxAliveTime < int(time.Now().Unix())-t.CompletionOn { 67 | MaxAliveTime = int(time.Now().Unix()) - t.CompletionOn 68 | MaxAliveSeeder = t.Hash 69 | MaxAliveName = t.Name 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | if MaxAliveTime != 0 { 78 | s.Client.DeleteTorrents(MaxAliveSeeder) 79 | fmt.Println("[" + s.Remark + "]删除超时种子." + MaxAliveName) 80 | return 81 | } 82 | 83 | //开始执行删除操作(第三圈,删除其中一个最古老的正在进行的任务.) 84 | MaxAliveTime = 0 85 | MaxAliveSeeder = "" 86 | MaxAliveName = "" 87 | if ts, err := s.Client.GetList(); err == nil { 88 | for _, t := range ts { 89 | for _, n := range cfg.Node { 90 | if n.Source == t.Category { 91 | if t.AmountLeft != 0 { 92 | if (int(time.Now().Unix()) - t.AddedOn) > s.Rule.MaxAliveTime { 93 | if MaxAliveTime < int(time.Now().Unix())-t.CompletionOn { 94 | MaxAliveTime = int(time.Now().Unix()) - t.CompletionOn 95 | MaxAliveSeeder = t.Hash 96 | MaxAliveName = t.Name 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | if MaxAliveTime != 0 { 106 | s.Client.DeleteTorrents(MaxAliveSeeder) 107 | fmt.Println("[" + s.Remark + "]删除超时种子." + MaxAliveName) 108 | return 109 | } 110 | } 111 | 112 | // 即使有空间但是没速度也应该执行清理 113 | if (s.Status.UpInfoSpeed + s.Status.DownInfoSpeed) < (512 * 1024) { 114 | // 执行和上面第三圈一样的操作 115 | DownloadCnt := 0 116 | MaxAliveTime := 0 117 | MaxAliveSeeder := "" 118 | MaxAliveName := "" 119 | if ts, err := s.Client.GetList(); err == nil { 120 | for _, t := range ts { 121 | for _, n := range cfg.Node { 122 | if n.Source == t.Category { 123 | if t.AmountLeft != 0 { 124 | DownloadCnt = DownloadCnt + 1 125 | if (int(time.Now().Unix()) - t.AddedOn) > s.Rule.MaxAliveTime { 126 | if MaxAliveTime > int(time.Now().Unix())-t.CompletionOn { 127 | MaxAliveTime = int(time.Now().Unix()) - t.CompletionOn 128 | MaxAliveSeeder = t.Hash 129 | MaxAliveName = t.Name 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | if MaxAliveTime != 0 && DownloadCnt > (s.Rule.ConcurrentDownload/2) { 139 | s.Client.DeleteTorrents(MaxAliveSeeder) 140 | fmt.Println("[" + s.Remark + "]删除超时种子." + MaxAliveName) 141 | return 142 | } 143 | } 144 | 145 | //fmt.Println("[" + s.Remark + "]无法完成清理.") 146 | } 147 | 148 | func (s *Server) ServerRuleTest() bool { 149 | TestStatus := "测试成功" 150 | 151 | if s.Rule.MaxDiskLatency <= s.Status.DiskLatency { 152 | TestStatus = "测试失败" 153 | } 154 | 155 | if s.Status.UpInfoSpeed >= s.Rule.MaxSpeed { 156 | TestStatus = "测试失败" 157 | } 158 | 159 | if s.Status.DownInfoSpeed >= s.Rule.MaxSpeed { 160 | TestStatus = "测试失败" 161 | } 162 | 163 | if s.Status.ConcurrentDownload >= s.Rule.ConcurrentDownload { 164 | TestStatus = "测试失败" 165 | } 166 | 167 | fmt.Printf("[%s][%s] 当前磁盘空间余量 %.2f[%.2f]GB,磁盘延迟 %d[%d] ms,上传速度 %.2f[%.2f],下载速度 %.2f[%.2f],同时任务 %d[%d] 个.\n", 168 | s.Remark, TestStatus, 169 | float64(s.Status.EstimatedQuota)/1073741824.0, float64(s.Status.FreeSpaceOnDisk)/1073741824, 170 | s.Status.DiskLatency, s.Rule.MaxDiskLatency, 171 | float64(s.Status.UpInfoSpeed)/1048576.0, float64(s.Rule.MaxSpeed)/1048576.0, 172 | float64(s.Status.DownInfoSpeed)/1048576.0, float64(s.Rule.MaxSpeed)/1048576.0, 173 | s.Status.ConcurrentDownload, s.Rule.ConcurrentDownload, 174 | ) 175 | 176 | if TestStatus == "测试失败" { 177 | return false 178 | } 179 | 180 | return true 181 | 182 | } 183 | 184 | func (s *Server) AddTorrentByURL(URL string, Size int, SpeedLimit int) bool { 185 | var options_add model.AddTorrentsOptions 186 | options_add.Savepath = "/downloads/" 187 | options_add.Category = strings.Split(strings.Split(URL, "//")[1], "/")[0] 188 | options_add.DlLimit = strconv.Itoa(SpeedLimit) 189 | options_add.UpLimit = strconv.Itoa(SpeedLimit) 190 | 191 | if ts, err := s.Client.GetList(); err == nil { 192 | for _, t := range ts { 193 | if t.Size == Size { 194 | //有同样大小的种子在一个机,容易产生混乱. 195 | //@TODO后期可以利用这个特性做辅粽功能,必须数据库有Size才可以. 196 | return false 197 | } 198 | } 199 | } 200 | 201 | //测试特殊网站规则(Beta),避免有些网站付费种太多. 202 | 203 | // HDTIME网站不足100G大小种子会被忽略. 204 | if strings.Contains(URL, "hdtime.org") { 205 | if Size < 100*1024*1024*1024 { 206 | return true 207 | } 208 | } 209 | 210 | if Size < s.Rule.MaxTaskSize && Size > s.Rule.MinTaskSize && s.ServerRuleTest() == true { 211 | //如果允许超量提交(即塞了这个任务后,并且任务完成后空间会负数,则不检查空间直接OK!),否则检查是否塞进去后还有空间剩余. 212 | //这个功能针对极小盘有很好的作用,因为极小盘很容易就会塞满,参数又不好调整. 213 | if s.Rule.DiskOverCommit == true || (s.Status.EstimatedQuota-Size) > (s.Rule.DiskThreshold/10) { 214 | if err := s.Client.AddURLs(URL, &options_add); err == nil { 215 | return true 216 | } 217 | } 218 | } 219 | return false 220 | } 221 | 222 | func (s *Server) CalcEstimatedQuota() { 223 | // 这里计算出来的是磁盘正在可以用的空间 224 | if r, err := s.Client.GetMainData(); err == nil { 225 | s.Status.DiskLatency = r.ServerState.AverageTimeQueue 226 | s.Status.FreeSpaceOnDisk = r.ServerState.FreeSpaceOnDisk 227 | s.Status.EstimatedQuota = r.ServerState.FreeSpaceOnDisk 228 | // 这里计算出来的是磁盘预期可以用的空间.(假设种子会全部下载) 229 | if ts, err := s.Client.GetList(); err == nil { 230 | s.Status.ConcurrentDownload = 0 231 | for _, t := range ts { 232 | if t.AmountLeft != 0 { 233 | s.Status.ConcurrentDownload++ 234 | } 235 | s.Status.EstimatedQuota -= t.AmountLeft 236 | } 237 | } else { 238 | //如果无法获取状态,直接让并行任务数显示最大以跳过规则. 239 | s.Status.ConcurrentDownload = 65535 240 | } 241 | } 242 | 243 | if r, err := s.Client.GetTransferInfo(); err == nil { 244 | s.Status.UpInfoSpeed = r.UpInfoSpeed 245 | s.Status.DownInfoSpeed = r.DlInfoSpeed 246 | } 247 | } 248 | 249 | func NewClientWrapper(baseURL string, username string, password string, remark string, rule config.ServerRule) Server { 250 | server, err := NewClient(baseURL, username, password) 251 | 252 | if err != nil { 253 | print("[" + remark + "]密码打错了,赶紧去修正.") 254 | } 255 | 256 | return Server{ 257 | Client: server, 258 | Rule: config.RawServerRule{ 259 | ConcurrentDownload: rule.ConcurrentDownload, 260 | DiskThreshold: int(rule.DiskThreshold * 1024 * 1024 * 1024), 261 | DiskOverCommit: rule.DiskOverCommit, 262 | MaxSpeed: int(rule.MaxSpeed * 1024 * 1024), 263 | MinAliveTime: rule.MinAliveTime, 264 | MaxAliveTime: rule.MaxAliveTime, 265 | MinTaskSize: int(rule.MinTaskSize * 1024 * 1024 * 1024), 266 | MaxTaskSize: int(rule.MaxTaskSize * 1024 * 1024 * 1024), 267 | MaxDiskLatency: rule.MaxDiskLatency, 268 | }, 269 | Remark: remark, 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/qbittorrent/pkg/model/preferences.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Preferences struct { 4 | // Currently selected language (e.g. en_GB for English) 5 | Locale string `json:"locale"` 6 | // True if a subfolder should be created when adding a torrent 7 | CreateSubfolderEnabled bool `json:"create_subfolder_enabled"` 8 | // True if torrents should be added in a Paused state 9 | StartPausedEnabled bool `json:"start_paused_enabled"` 10 | // No documentation provided 11 | AutoDeleteMode int `json:"auto_delete_mode"` 12 | // True if disk space should be pre-allocated for all files 13 | PreallocateAll bool `json:"preallocate_all"` 14 | // True if ".!qB" should be appended to incomplete files 15 | IncompleteFilesExt bool `json:"incomplete_files_ext"` 16 | // True if Automatic Torrent Management is enabled by default 17 | AutoTmmEnabled bool `json:"auto_tmm_enabled"` 18 | // True if torrent should be relocated when its Category changes 19 | TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled"` 20 | // True if torrent should be relocated when the default save path changes 21 | SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled"` 22 | // True if torrent should be relocated when its Category's save path changes 23 | CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled"` 24 | // Default save path for torrents, separated by slashes 25 | SavePath string `json:"save_path"` 26 | // True if folder for incomplete torrents is enabled 27 | TempPathEnabled bool `json:"temp_path_enabled"` 28 | // Path for incomplete torrents, separated by slashes 29 | TempPath string `json:"temp_path"` 30 | // Property: directory to watch for torrent files, value: where torrents loaded from this directory should be downloaded to (see list of possible values below). Slashes are used as path separators; multiple key/value pairs can be specified 31 | ScanDirs map[string]interface{} `json:"scan_dirs"` 32 | // Path to directory to copy .torrent files to. Slashes are used as path separators 33 | ExportDir string `json:"export_dir"` 34 | // Path to directory to copy .torrent files of completed downloads to. Slashes are used as path separators 35 | ExportDirFin string `json:"export_dir_fin"` 36 | // True if e-mail notification should be enabled 37 | MailNotificationEnabled bool `json:"mail_notification_enabled"` 38 | // e-mail where notifications should originate from 39 | MailNotificationSender string `json:"mail_notification_sender"` 40 | // e-mail to send notifications to 41 | MailNotificationEmail string `json:"mail_notification_email"` 42 | // smtp server for e-mail notifications 43 | MailNotificationSmtp string `json:"mail_notification_smtp"` 44 | // True if smtp server requires SSL connection 45 | MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled"` 46 | // True if smtp server requires authentication 47 | MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled"` 48 | // Username for smtp authentication 49 | MailNotificationUsername string `json:"mail_notification_username"` 50 | // Password for smtp authentication 51 | MailNotificationPassword string `json:"mail_notification_password"` 52 | // True if external program should be run after torrent has finished downloading 53 | AutorunEnabled bool `json:"autorun_enabled"` 54 | // Program path/name/arguments to run if autorun_enabled is enabled; path is separated by slashes; you can use %f and %n arguments, which will be expanded by qBittorent as path_to_torrent_file and torrent_name (from the GUI; not the .torrent file name) respectively 55 | AutorunProgram string `json:"autorun_program"` 56 | // True if torrent queuing is enabled 57 | QueueingEnabled bool `json:"queueing_enabled"` 58 | // Maximum number of active simultaneous downloads 59 | MaxActiveDownloads int `json:"max_active_downloads"` 60 | // Maximum number of active simultaneous downloads and uploads 61 | MaxActiveTorrents int `json:"max_active_torrents"` 62 | // Maximum number of active simultaneous uploads 63 | MaxActiveUploads int `json:"max_active_uploads"` 64 | // If true torrents w/o any activity (stalled ones) will not be counted towards max_active_* limits; see dont_count_slow_torrents for more information 65 | DontCountSlowTorrents bool `json:"dont_count_slow_torrents"` 66 | // Download rate in KiB/s for a torrent to be considered "slow" 67 | SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold"` 68 | // Upload rate in KiB/s for a torrent to be considered "slow" 69 | SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold"` 70 | // Seconds a torrent should be inactive before considered "slow" 71 | SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer"` 72 | // True if share ratio limit is enabled 73 | MaxRatioEnabled bool `json:"max_ratio_enabled"` 74 | // Get the global share ratio limit 75 | MaxRatio float64 `json:"max_ratio"` 76 | // Action performed when a torrent reaches the maximum share ratio. See list of possible values here below. 77 | MaxRatioAct MaxRatioAction `json:"max_ratio_act"` 78 | // Port for incoming connections 79 | ListenPort int `json:"listen_port"` 80 | // True if UPnP/NAT-PMP is enabled 81 | Upnp bool `json:"upnp"` 82 | // True if the port is randomly selected 83 | RandomPort bool `json:"random_port"` 84 | // Global download speed limit in KiB/s; -1 means no limit is applied 85 | DlLimit int `json:"dl_limit"` 86 | // Global upload speed limit in KiB/s; -1 means no limit is applied 87 | UpLimit int `json:"up_limit"` 88 | // Maximum global number of simultaneous connections 89 | MaxConnec int `json:"max_connec"` 90 | // Maximum number of simultaneous connections per torrent 91 | MaxConnecPerTorrent int `json:"max_connec_per_torrent"` 92 | // Maximum number of upload slots 93 | MaxUploads int `json:"max_uploads"` 94 | // Maximum number of upload slots per torrent 95 | MaxUploadsPerTorrent int `json:"max_uploads_per_torrent"` 96 | // True if uTP protocol should be enabled; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 97 | EnableUtp bool `json:"enable_utp"` 98 | // True if [du]l_limit should be applied to uTP connections; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 99 | LimitUtpRate bool `json:"limit_utp_rate"` 100 | // True if [du]l_limit should be applied to estimated TCP overhead (service data: e.g. packet headers) 101 | LimitTcpOverhead bool `json:"limit_tcp_overhead"` 102 | // True if [du]l_limit should be applied to peers on the LAN 103 | LimitLanPeers bool `json:"limit_lan_peers"` 104 | // Alternative global download speed limit in KiB/s 105 | AltDlLimit int `json:"alt_dl_limit"` 106 | // Alternative global upload speed limit in KiB/s 107 | AltUpLimit int `json:"alt_up_limit"` 108 | // True if alternative limits should be applied according to schedule 109 | SchedulerEnabled bool `json:"scheduler_enabled"` 110 | // Scheduler starting hour 111 | ScheduleFromHour int `json:"schedule_from_hour"` 112 | // Scheduler starting minute 113 | ScheduleFromMin int `json:"schedule_from_min"` 114 | // Scheduler ending hour 115 | ScheduleToHour int `json:"schedule_to_hour"` 116 | // Scheduler ending minute 117 | ScheduleToMin int `json:"schedule_to_min"` 118 | // Scheduler days. See possible values here below 119 | SchedulerDays int `json:"scheduler_days"` 120 | // True if DHT is enabled 121 | Dht bool `json:"dht"` 122 | // True if DHT port should match TCP port 123 | DhtSameAsBT bool `json:"dhtSameAsBT"` 124 | // DHT port if dhtSameAsBT is false 125 | DhtPort int `json:"dht_port"` 126 | // True if PeX is enabled 127 | Pex bool `json:"pex"` 128 | // True if LSD is enabled 129 | Lsd bool `json:"lsd"` 130 | // See list of possible values here below 131 | Encryption int `json:"encryption"` 132 | // If true anonymous mode will be enabled; read more here; this option is only available in qBittorent built against libtorrent version 0.16.X and higher 133 | AnonymousMode bool `json:"anonymous_mode"` 134 | // See list of possible values here below 135 | ProxyType int `json:"proxy_type"` 136 | // Proxy IP address or domain name 137 | ProxyIp string `json:"proxy_ip"` 138 | // Proxy port 139 | ProxyPort int `json:"proxy_port"` 140 | // True if peer and web seed connections should be proxified; this option will have any effect only in qBittorent built against libtorrent version 0.16.X and higher 141 | ProxyPeerConnections bool `json:"proxy_peer_connections"` 142 | // True if the connections not supported by the proxy are disabled 143 | ForceProxy bool `json:"force_proxy"` 144 | // True proxy requires authentication; doesn't apply to SOCKS4 proxies 145 | ProxyAuthEnabled bool `json:"proxy_auth_enabled"` 146 | // Username for proxy authentication 147 | ProxyUsername string `json:"proxy_username"` 148 | // Password for proxy authentication 149 | ProxyPassword string `json:"proxy_password"` 150 | // True if external IP filter should be enabled 151 | IpFilterEnabled bool `json:"ip_filter_enabled"` 152 | // Path to IP filter file (.dat, .p2p, .p2b files are supported); path is separated by slashes 153 | IpFilterPath string `json:"ip_filter_path"` 154 | // True if IP filters are applied to trackers 155 | IpFilterTrackers bool `json:"ip_filter_trackers"` 156 | // Comma-separated list of domains to accept when performing Host header validation 157 | WebUiDomainList string `json:"web_ui_domain_list"` 158 | // IP address to use for the WebUI 159 | WebUiAddress string `json:"web_ui_address"` 160 | // WebUI port 161 | WebUiPort int `json:"web_ui_port"` 162 | // True if UPnP is used for the WebUI port 163 | WebUiUpnp bool `json:"web_ui_upnp"` 164 | // WebUI username 165 | WebUiUsername string `json:"web_ui_username"` 166 | // For API ≥ v2.3.0: Plaintext WebUI password, not readable, write-only. For API < v2.3.0: MD5 hash of WebUI password, hash is generated from the following string: username:Web UI Access:plain_text_web_ui_password 167 | WebUiPassword string `json:"web_ui_password"` 168 | // True if WebUI CSRF protection is enabled 169 | WebUiCsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled"` 170 | // True if WebUI clickjacking protection is enabled 171 | WebUiClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled"` 172 | // True if authentication challenge for loopback address (127.0.0.1) should be disabled 173 | BypassLocalAuth bool `json:"bypass_local_auth"` 174 | // True if webui authentication should be bypassed for clients whose ip resides within (at least) one of the subnets on the whitelist 175 | BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled"` 176 | // (White)list of ipv4/ipv6 subnets for which webui authentication should be bypassed; list entries are separated by commas 177 | BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist"` 178 | // True if an alternative WebUI should be used 179 | AlternativeWebuiEnabled bool `json:"alternative_webui_enabled"` 180 | // File path to the alternative WebUI 181 | AlternativeWebuiPath string `json:"alternative_webui_path"` 182 | // True if WebUI HTTPS access is enabled 183 | UseHttps bool `json:"use_https"` 184 | // SSL keyfile contents (this is a not a path) 185 | SslKey string `json:"ssl_key"` 186 | // SSL certificate contents (this is a not a path) 187 | SslCert string `json:"ssl_cert"` 188 | // True if server DNS should be updated dynamically 189 | DyndnsEnabled bool `json:"dyndns_enabled"` 190 | // See list of possible values here below 191 | DyndnsService int `json:"dyndns_service"` 192 | // Username for DDNS service 193 | DyndnsUsername string `json:"dyndns_username"` 194 | // Password for DDNS service 195 | DyndnsPassword string `json:"dyndns_password"` 196 | // Your DDNS domain name 197 | DyndnsDomain string `json:"dyndns_domain"` 198 | // RSS refresh interval 199 | RssRefreshInterval int `json:"rss_refresh_interval"` 200 | // Max stored articles per RSS feed 201 | RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed"` 202 | // Enable processing of RSS feeds 203 | RssProcessingEnabled bool `json:"rss_processing_enabled"` 204 | // Enable auto-downloading of torrents from the RSS feeds 205 | RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled"` 206 | } 207 | 208 | type MaxRatioAction int 209 | 210 | const ( 211 | ActionPause MaxRatioAction = 0 212 | ActionRemove = 1 213 | ) 214 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= 5 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 6 | github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk= 7 | github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= 8 | github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 13 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 14 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 15 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 16 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 17 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 18 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 19 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 20 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 21 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 22 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 23 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 24 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 25 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 26 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 27 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 28 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 29 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 30 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 31 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 32 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 33 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 34 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 35 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 36 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 37 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 38 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 39 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 40 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 41 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 43 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 46 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 47 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 48 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 49 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 50 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 51 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 52 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 53 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 54 | github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= 55 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 56 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 59 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 62 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 63 | github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= 64 | github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 65 | github.com/mmcdole/gofeed v1.1.0 h1:T2WrGLVJRV04PY2qwhEJLHCt9JiCtBhb6SmC8ZvJH08= 66 | github.com/mmcdole/gofeed v1.1.0/go.mod h1:PPiVwgDXLlz2N83KB4TrIim2lyYM5Zn7ZWH9Pi4oHUk= 67 | github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= 68 | github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 70 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 71 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 72 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 73 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 74 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 75 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 77 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 78 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 81 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 82 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 83 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 84 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 85 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 86 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 87 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 88 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 89 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 90 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 91 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 92 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 93 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 94 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 95 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 101 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 102 | github.com/tomcraven/gotable v0.0.0-20160801225336-eb315dabcfbd h1:h6YqVioGaaY5j8ZVa23Lh0kHeUn6t1tfMdNuAS0c95o= 103 | github.com/tomcraven/gotable v0.0.0-20160801225336-eb315dabcfbd/go.mod h1:iYqP0v9FitPukOx9Z6IDllBwpsYLf4aiOZHmwjyRRWI= 104 | github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 105 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 106 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 107 | github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= 108 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 109 | github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= 110 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 111 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 112 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 113 | go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= 114 | go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= 115 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 116 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 117 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 118 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 119 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 120 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 121 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 122 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 123 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 124 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 125 | golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= 126 | golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 127 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 131 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 133 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= 142 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 144 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 145 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 146 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 147 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 148 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 149 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 150 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 151 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 152 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 153 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 154 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 155 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 159 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 160 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 161 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | --------------------------------------------------------------------------------