mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
325 lines
7.0 KiB
Go
325 lines
7.0 KiB
Go
package bt
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/GopeedLab/gopeed/internal/controller"
|
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
|
"github.com/GopeedLab/gopeed/pkg/util"
|
|
"github.com/anacrolix/torrent"
|
|
"github.com/anacrolix/torrent/metainfo"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
client *torrent.Client
|
|
lock sync.Mutex
|
|
torrentDirMap = make(map[string]string)
|
|
ftMap = make(map[string]*fileTorrentImpl)
|
|
)
|
|
|
|
type Fetcher struct {
|
|
ctl *controller.Controller
|
|
config *config
|
|
|
|
torrent *torrent.Torrent
|
|
meta *fetcher.FetcherMeta
|
|
|
|
torrentReady atomic.Bool
|
|
torrentDrop atomic.Bool
|
|
create atomic.Bool
|
|
progress fetcher.Progress
|
|
}
|
|
|
|
func (f *Fetcher) Name() string {
|
|
return "bt"
|
|
}
|
|
|
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|
f.ctl = ctl
|
|
if f.meta == nil {
|
|
f.meta = &fetcher.FetcherMeta{}
|
|
}
|
|
exist := f.ctl.GetConfig(&f.config)
|
|
if !exist {
|
|
f.config = &config{
|
|
ListenPort: 0,
|
|
Trackers: []string{},
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (f *Fetcher) initClient() (err error) {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
if client != nil {
|
|
return
|
|
}
|
|
|
|
cfg := torrent.NewDefaultClientConfig()
|
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
|
cfg.ListenPort = f.config.ListenPort
|
|
cfg.HTTPProxy = util.ProxyUrlToHandler(f.ctl.ProxyUrl)
|
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
|
ClientBaseDir: cfg.DataDir,
|
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
|
if dir, ok := torrentDirMap[infoHash.String()]; ok {
|
|
ft.setTorrentDir(dir)
|
|
}
|
|
ftMap[infoHash.String()] = ft
|
|
},
|
|
})
|
|
client, err = torrent.NewClient(cfg)
|
|
return
|
|
}
|
|
|
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := f.addTorrent(req); err != nil {
|
|
return err
|
|
}
|
|
f.updateRes()
|
|
f.meta.Req = req
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
|
f.create.Store(true)
|
|
f.meta.Opts = opts
|
|
if f.meta.Res != nil {
|
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Start() (err error) {
|
|
if !f.torrentReady.Load() {
|
|
if err = f.addTorrent(f.meta.Req); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
|
ft.setTorrentDir(f.meta.FolderPath())
|
|
}
|
|
files := f.torrent.Files()
|
|
// If the user does not specify the file to download, all files will be downloaded by default
|
|
if len(f.meta.Opts.SelectFiles) == 0 {
|
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
|
for i := range files {
|
|
f.meta.Opts.SelectFiles[i] = i
|
|
}
|
|
}
|
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
|
f.torrent.DownloadAll()
|
|
} else {
|
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
file := files[selectIndex]
|
|
file.Download()
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (f *Fetcher) Pause() (err error) {
|
|
f.torrentReady.Store(false)
|
|
f.safeDrop()
|
|
return
|
|
}
|
|
|
|
func (f *Fetcher) Close() (err error) {
|
|
f.torrentDrop.Store(false)
|
|
f.safeDrop()
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) safeDrop() {
|
|
defer func() {
|
|
// ignore panic
|
|
_ = recover()
|
|
}()
|
|
|
|
f.torrent.Drop()
|
|
}
|
|
|
|
func (f *Fetcher) Wait() (err error) {
|
|
for {
|
|
if f.torrentDrop.Load() {
|
|
break
|
|
}
|
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
|
done := true
|
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
file := f.torrent.Files()[selectIndex]
|
|
if file.BytesCompleted() < file.Length() {
|
|
done = false
|
|
break
|
|
}
|
|
}
|
|
if done {
|
|
// remove unselected files
|
|
for i, file := range f.torrent.Files() {
|
|
selected := false
|
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
if i == selectIndex {
|
|
selected = true
|
|
break
|
|
}
|
|
}
|
|
if !selected {
|
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(time.Millisecond * 500)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|
return f.meta
|
|
}
|
|
|
|
func (f *Fetcher) Stats() any {
|
|
stats := f.torrent.Stats()
|
|
baseStats := &bt.Stats{
|
|
TotalPeers: stats.TotalPeers,
|
|
ActivePeers: stats.ActivePeers,
|
|
ConnectedSeeders: stats.ConnectedSeeders,
|
|
}
|
|
return baseStats
|
|
}
|
|
|
|
func (f *Fetcher) Progress() fetcher.Progress {
|
|
if !f.torrentReady.Load() {
|
|
return f.progress
|
|
}
|
|
for i := range f.progress {
|
|
selectIndex := f.meta.Opts.SelectFiles[i]
|
|
file := f.torrent.Files()[selectIndex]
|
|
f.progress[i] = file.BytesCompleted()
|
|
}
|
|
return f.progress
|
|
}
|
|
|
|
func (f *Fetcher) updateRes() {
|
|
res := &base.Resource{
|
|
Name: f.torrent.Name(),
|
|
Range: true,
|
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
|
Hash: f.torrent.InfoHash().String(),
|
|
}
|
|
f.torrent.PeerConns()
|
|
for i, file := range f.torrent.Files() {
|
|
res.Files[i] = &base.FileInfo{
|
|
Name: filepath.Base(file.DisplayPath()),
|
|
Path: util.Dir(file.Path()),
|
|
Size: file.Length(),
|
|
}
|
|
}
|
|
res.CalcSize(nil)
|
|
f.meta.Res = res
|
|
if f.meta.Opts != nil {
|
|
f.meta.Opts.InitSelectFiles(len(res.Files))
|
|
}
|
|
}
|
|
|
|
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|
if err = f.initClient(); err != nil {
|
|
return
|
|
}
|
|
schema := util.ParseSchema(req.URL)
|
|
if schema == "MAGNET" {
|
|
f.torrent, err = client.AddMagnet(req.URL)
|
|
} else if schema == "APPLICATION/X-BITTORRENT" {
|
|
_, data := util.ParseDataUri(req.URL)
|
|
buf := bytes.NewBuffer(data)
|
|
var metaInfo *metainfo.MetaInfo
|
|
metaInfo, err = metainfo.Load(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.torrent, err = client.AddTorrent(metaInfo)
|
|
} else {
|
|
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// use map to deduplicate
|
|
trackers := make(map[string]bool)
|
|
if req.Extra != nil {
|
|
extra := req.Extra.(*bt.ReqExtra)
|
|
if len(extra.Trackers) > 0 {
|
|
for _, tracker := range extra.Trackers {
|
|
trackers[tracker] = true
|
|
}
|
|
}
|
|
}
|
|
if len(f.config.Trackers) > 0 {
|
|
for _, tracker := range f.config.Trackers {
|
|
trackers[tracker] = true
|
|
}
|
|
}
|
|
if len(trackers) > 0 {
|
|
announceList := make([][]string, 0)
|
|
for tracker := range trackers {
|
|
announceList = append(announceList, []string{tracker})
|
|
}
|
|
f.torrent.AddTrackers(announceList)
|
|
}
|
|
<-f.torrent.GotInfo()
|
|
f.torrentReady.Store(true)
|
|
return
|
|
}
|
|
|
|
type FetcherBuilder struct {
|
|
}
|
|
|
|
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
|
|
|
func (fb *FetcherBuilder) Schemes() []string {
|
|
return schemes
|
|
}
|
|
|
|
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
|
return &Fetcher{}
|
|
}
|
|
|
|
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
|
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
|
return &Fetcher{
|
|
meta: meta,
|
|
}
|
|
}
|
|
}
|
|
|
|
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
|
func parseBep20() string {
|
|
s := strings.ReplaceAll(base.Version, ".", "")
|
|
if len(s) < 4 {
|
|
s += strings.Repeat("0", 4-len(s))
|
|
}
|
|
return s
|
|
}
|