mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
feat: torrent seed config (#599)
This commit is contained in:
22 files changed
+456
-144
No files matched your search
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
GetConfig func(v any) bool
|
||||
GetConfig func(v any)
|
||||
ProxyConfig *base.DownloaderProxyConfig
|
||||
FileController
|
||||
//ContextDialer() (proxy.Dialer, error)
|
||||
@@ -22,10 +22,8 @@ type DefaultFileController struct {
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
GetConfig: func(v any) {},
|
||||
FileController: &DefaultFileController{},
|
||||
GetConfig: func(v any) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// Fetcher defines the interface for a download protocol.
|
||||
// One fetcher for each download task
|
||||
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||
type Fetcher interface {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
|
||||
Setup(ctl *controller.Controller)
|
||||
// Resolve resource info from request
|
||||
Resolve(req *base.Request) error
|
||||
@@ -31,6 +28,12 @@ type Fetcher interface {
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type Uploader interface {
|
||||
Upload() error
|
||||
UploadedBytes() int64
|
||||
WaitUpload() error
|
||||
}
|
||||
|
||||
// FetcherMeta defines the meta information of a fetcher.
|
||||
type FetcherMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
@@ -70,11 +73,15 @@ func (m *FetcherMeta) RootDirPath() string {
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
type FetcherBuilder interface {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
// Schemes returns the schemes supported by the fetcher.
|
||||
Schemes() []string
|
||||
// Build returns a new fetcher.
|
||||
Build() Fetcher
|
||||
|
||||
// DefaultConfig returns the default configuration of the protocol.
|
||||
DefaultConfig() any
|
||||
// Store fetcher
|
||||
Store(fetcher Fetcher) (any, error)
|
||||
// Restore fetcher
|
||||
|
||||
@@ -3,4 +3,10 @@ package bt
|
||||
type config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
Trackers []string `json:"trackers"`
|
||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||
SeedKeep bool `json:"seedKeep`
|
||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||
SeedRatio float64 `json:"seedRatio"`
|
||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||
SeedTime int64 `json:"seedTime"`
|
||||
}
|
||||
+162
-55
@@ -30,15 +30,12 @@ type Fetcher struct {
|
||||
|
||||
torrent *torrent.Torrent
|
||||
meta *fetcher.FetcherMeta
|
||||
data *fetcherData
|
||||
|
||||
torrentReady atomic.Bool
|
||||
torrentDrop atomic.Bool
|
||||
create atomic.Bool
|
||||
progress fetcher.Progress
|
||||
}
|
||||
|
||||
func (f *Fetcher) Name() string {
|
||||
return "bt"
|
||||
torrentReady atomic.Bool
|
||||
torrentDrop atomic.Bool
|
||||
torrentUpload atomic.Bool
|
||||
uploadDoneCh chan any
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
@@ -46,13 +43,10 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
ListenPort: 0,
|
||||
Trackers: []string{},
|
||||
}
|
||||
if f.data == nil {
|
||||
f.data = &fetcherData{}
|
||||
}
|
||||
f.ctl.GetConfig(&f.config)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,6 +59,7 @@ func (f *Fetcher) initClient() (err error) {
|
||||
}
|
||||
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.Seed = true
|
||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||
cfg.ListenPort = f.config.ListenPort
|
||||
@@ -92,11 +87,11 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
f.uploadDoneCh = make(chan any, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -111,13 +106,15 @@ func (f *Fetcher) Start() (err error) {
|
||||
}
|
||||
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
|
||||
if f.data.Progress == nil {
|
||||
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.data.Progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||
}
|
||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||
f.torrent.DownloadAll()
|
||||
} else {
|
||||
@@ -126,7 +123,6 @@ func (f *Fetcher) Start() (err error) {
|
||||
file.Download()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,8 +133,9 @@ func (f *Fetcher) Pause() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Close() (err error) {
|
||||
f.torrentDrop.Store(false)
|
||||
f.torrentDrop.Store(true)
|
||||
f.safeDrop()
|
||||
f.uploadDoneCh <- nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -151,21 +148,41 @@ func (f *Fetcher) safeDrop() {
|
||||
f.torrent.Drop()
|
||||
}
|
||||
|
||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Stats() any {
|
||||
stats := f.torrent.Stats()
|
||||
return &bt.Stats{
|
||||
TotalPeers: stats.TotalPeers,
|
||||
ActivePeers: stats.ActivePeers,
|
||||
ConnectedSeeders: stats.ConnectedSeeders,
|
||||
SeedBytes: f.data.SeedBytes,
|
||||
SeedRatio: f.seedRadio(),
|
||||
SeedTime: f.data.SeedTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
if !f.torrentReady.Load() {
|
||||
return f.data.Progress
|
||||
}
|
||||
for i := range f.data.Progress {
|
||||
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
f.data.Progress[i] = file.BytesCompleted()
|
||||
}
|
||||
return f.data.Progress
|
||||
}
|
||||
|
||||
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 {
|
||||
if f.isDone() {
|
||||
// remove unselected files
|
||||
for i, file := range f.torrent.Files() {
|
||||
selected := false
|
||||
@@ -187,30 +204,14 @@ func (f *Fetcher) Wait() (err error) {
|
||||
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]
|
||||
func (f *Fetcher) isDone() bool {
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
f.progress[i] = file.BytesCompleted()
|
||||
if file.BytesCompleted() < file.Length() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return f.progress
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Fetcher) updateRes() {
|
||||
@@ -235,6 +236,78 @@ func (f *Fetcher) updateRes() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) Upload() (err error) {
|
||||
return f.addTorrent(f.meta.Req)
|
||||
}
|
||||
|
||||
func (f *Fetcher) doUpload() {
|
||||
if f.torrentUpload.Load() {
|
||||
return
|
||||
}
|
||||
f.torrentUpload.Store(true)
|
||||
|
||||
// Check and update seed data
|
||||
lastData := &fetcherData{
|
||||
SeedBytes: f.data.SeedBytes,
|
||||
SeedTime: f.data.SeedTime,
|
||||
}
|
||||
var doneTime int64 = 0
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if f.torrentDrop.Load() {
|
||||
break
|
||||
}
|
||||
|
||||
if !f.torrentReady.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
stats := f.torrent.Stats()
|
||||
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
||||
|
||||
// Check is download complete, if not don't check and stop seeding
|
||||
if !f.isDone() {
|
||||
continue
|
||||
}
|
||||
if doneTime == 0 {
|
||||
doneTime = time.Now().Unix()
|
||||
}
|
||||
f.data.SeedTime = lastData.SeedTime + time.Now().Unix() - doneTime
|
||||
|
||||
// If the seed forever is true, keep seeding
|
||||
if f.config.SeedKeep {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the seed ratio is reached, stop seeding
|
||||
if f.config.SeedRatio > 0 {
|
||||
seedRadio := f.seedRadio()
|
||||
if seedRadio >= f.config.SeedRatio {
|
||||
f.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the seed time is reached, stop seeding
|
||||
if f.config.SeedTime > 0 {
|
||||
if f.data.SeedTime >= f.config.SeedTime {
|
||||
f.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) UploadedBytes() int64 {
|
||||
return f.data.SeedBytes
|
||||
}
|
||||
|
||||
func (f *Fetcher) WaitUpload() (err error) {
|
||||
<-f.uploadDoneCh
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||
return
|
||||
@@ -285,14 +358,36 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
}
|
||||
<-f.torrent.GotInfo()
|
||||
f.torrentReady.Store(true)
|
||||
|
||||
go f.doUpload()
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) seedRadio() float64 {
|
||||
bytesRead := f.data.Progress.TotalDownloaded()
|
||||
if bytesRead <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(f.data.SeedBytes) / float64(bytesRead)
|
||||
}
|
||||
|
||||
type fetcherData struct {
|
||||
Progress fetcher.Progress
|
||||
SeedBytes int64
|
||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||
SeedTime int64
|
||||
}
|
||||
|
||||
type FetcherBuilder struct {
|
||||
}
|
||||
|
||||
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
||||
|
||||
func (fb *FetcherBuilder) Name() string {
|
||||
return "bt"
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Schemes() []string {
|
||||
return schemes
|
||||
}
|
||||
@@ -301,14 +396,26 @@ func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) DefaultConfig() any {
|
||||
return &config{
|
||||
ListenPort: 0,
|
||||
Trackers: []string{},
|
||||
SeedKeep: false,
|
||||
SeedRatio: 1.0,
|
||||
SeedTime: 120 * 60,
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
return nil, nil
|
||||
_f := f.(*Fetcher)
|
||||
return _f.data, 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 &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
return &Fetcher{
|
||||
meta: meta,
|
||||
data: v.(*fetcherData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,13 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
}
|
||||
|
||||
func buildFetcher() fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fb := new(FetcherBuilder)
|
||||
fetcher := fb.Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
|
||||
@@ -113,11 +118,8 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
}}
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||
}
|
||||
newController.ProxyConfig = proxyConfig
|
||||
fetcher.Setup(newController)
|
||||
|
||||
@@ -64,23 +64,13 @@ type Fetcher struct {
|
||||
eg *errgroup.Group
|
||||
}
|
||||
|
||||
func (f *Fetcher) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
f.ctl = ctl
|
||||
f.doneCh = make(chan error, 1)
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 1,
|
||||
}
|
||||
}
|
||||
f.ctl.GetConfig(&f.config)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -471,6 +461,10 @@ type FetcherBuilder struct {
|
||||
|
||||
var schemes = []string{"HTTP", "HTTPS"}
|
||||
|
||||
func (fb *FetcherBuilder) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Schemes() []string {
|
||||
return schemes
|
||||
}
|
||||
@@ -479,6 +473,13 @@ func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) DefaultConfig() any {
|
||||
return &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
_f := f.(*Fetcher)
|
||||
return &fetcherData{
|
||||
|
||||
@@ -359,19 +359,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
||||
}
|
||||
|
||||
func buildFetcher() *Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fb := new(FetcherBuilder)
|
||||
fetcher := fb.Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher.(*Fetcher)
|
||||
}
|
||||
|
||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
|
||||
@@ -150,6 +150,9 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = make(map[string]any)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
||||
+82
-25
@@ -42,12 +42,16 @@ var (
|
||||
type Listener func(event *Event)
|
||||
|
||||
type Progress struct {
|
||||
// 下载耗时(纳秒)
|
||||
// Total download time(ns)
|
||||
Used int64 `json:"used"`
|
||||
// 每秒下载字节数
|
||||
// Download speed(bytes/s)
|
||||
Speed int64 `json:"speed"`
|
||||
// 已下载的字节数
|
||||
// Downloaded size(bytes)
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
// Uploaded speed(bytes/s)
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
// Uploaded size(bytes)
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
@@ -124,11 +128,14 @@ func (d *Downloader) Setup() error {
|
||||
}
|
||||
// init default config
|
||||
d.cfg.DownloaderStoreConfig.Init()
|
||||
// init protocol config, if not exist, use default config
|
||||
for _, fb := range d.fetcherBuilders {
|
||||
f := fb.Build()
|
||||
d.setupFetcher(f)
|
||||
|
||||
protocol := fb.Name()
|
||||
if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok {
|
||||
d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fb.DefaultConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// load tasks from storage
|
||||
var tasks []*Task
|
||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||
@@ -166,6 +173,22 @@ func (d *Downloader) Setup() error {
|
||||
}
|
||||
d.extensions = extensions
|
||||
|
||||
// handle upload
|
||||
go func() {
|
||||
for _, task := range d.tasks {
|
||||
if task.Status == base.DownloadStatusDone && task.Uploading {
|
||||
if err := d.restoreFetcher(task); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("task upload restore fetcher failed, task id: %s", task.ID)
|
||||
}
|
||||
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||
if err := uploader.Upload(); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("task upload failed, task id: %s", task.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// calculate download speed every tick
|
||||
go func() {
|
||||
for !d.closed.Load() {
|
||||
@@ -174,18 +197,27 @@ func (d *Downloader) Setup() error {
|
||||
func() {
|
||||
task.statusLock.Lock()
|
||||
defer task.statusLock.Unlock()
|
||||
if task.Status != base.DownloadStatusRunning {
|
||||
if task.Status != base.DownloadStatusRunning && !task.Uploading {
|
||||
return
|
||||
}
|
||||
// check if task is deleted
|
||||
if d.GetTask(task.ID) == nil {
|
||||
if d.GetTask(task.ID) == nil || task.fetcher == nil {
|
||||
return
|
||||
}
|
||||
|
||||
current := task.fetcher.Progress().TotalDownloaded()
|
||||
task.Progress.Used = task.timer.Used()
|
||||
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
|
||||
task.Progress.Downloaded = current
|
||||
tick := float64(d.cfg.RefreshInterval) / 1000
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
task.Progress.Used = task.timer.Used()
|
||||
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, tick)
|
||||
task.Progress.Downloaded = current
|
||||
}
|
||||
if task.Uploading {
|
||||
uploader := task.fetcher.(fetcher.Uploader)
|
||||
currentUploaded := uploader.UploadedBytes()
|
||||
task.Progress.UploadSpeed = task.calcSpeed(currentUploaded-task.Progress.Uploaded, tick)
|
||||
task.Progress.Uploaded = currentUploaded
|
||||
}
|
||||
d.emit(EventKeyProgress, task)
|
||||
|
||||
// store fetcher progress
|
||||
@@ -220,10 +252,10 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
||||
return nil, ErrUnSupportedProtocol
|
||||
}
|
||||
|
||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||
func (d *Downloader) setupFetcher(fb fetcher.FetcherBuilder, fetcher fetcher.Fetcher) {
|
||||
ctl := controller.NewController()
|
||||
ctl.GetConfig = func(v any) bool {
|
||||
return d.getProtocolConfig(fetcher.Name(), v)
|
||||
ctl.GetConfig = func(v any) {
|
||||
d.getProtocolConfig(fb.Name(), v)
|
||||
}
|
||||
ctl.ProxyConfig = d.cfg.Proxy
|
||||
fetcher.Setup(ctl)
|
||||
@@ -641,6 +673,30 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
|
||||
|
||||
// wait task done
|
||||
func (d *Downloader) watch(task *Task) {
|
||||
// wait task upload done
|
||||
if task.Uploading {
|
||||
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||
go func() {
|
||||
err := uploader.WaitUpload()
|
||||
if err != nil {
|
||||
d.Logger.Warn().Err(err).Msgf("task wait upload failed, task id: %s", task.ID)
|
||||
}
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
// Check if the task is deleted
|
||||
if d.GetTask(task.ID) != nil {
|
||||
task.Uploading = false
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if task.Status == base.DownloadStatusDone {
|
||||
return
|
||||
}
|
||||
|
||||
err := task.fetcher.Wait()
|
||||
if err != nil {
|
||||
d.doOnError(task, err)
|
||||
@@ -719,7 +775,7 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
if task.fetcher == nil {
|
||||
task.fetcher = fb.Build()
|
||||
}
|
||||
d.setupFetcher(task.fetcher)
|
||||
d.setupFetcher(fb, task.fetcher)
|
||||
if task.fetcher.Meta().Req == nil {
|
||||
task.fetcher.Meta().Req = task.Meta.Req
|
||||
}
|
||||
@@ -734,7 +790,7 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
||||
func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
||||
if opts == nil {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
@@ -742,7 +798,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
}
|
||||
|
||||
meta := fetcher.Meta()
|
||||
meta := f.Meta()
|
||||
meta.Opts = opts
|
||||
if opts.Path == "" {
|
||||
storeConfig, err := d.GetConfig()
|
||||
@@ -752,19 +808,20 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
opts.Path = storeConfig.DownloadDir
|
||||
}
|
||||
|
||||
fb, err := d.parseFb(fetcher.Meta().Req.URL)
|
||||
fb, err := d.parseFb(f.Meta().Req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := NewTask()
|
||||
task.fetcherBuilder = fb
|
||||
task.fetcher = fetcher
|
||||
task.Protocol = fetcher.Name()
|
||||
task.Meta = fetcher.Meta()
|
||||
task.fetcher = f
|
||||
task.Protocol = fb.Name()
|
||||
task.Meta = f.Meta()
|
||||
task.Progress = &Progress{}
|
||||
_, task.Uploading = f.(fetcher.Uploader)
|
||||
initTask(task)
|
||||
if err = fetcher.Create(opts); err != nil {
|
||||
if err = f.Create(opts); err != nil {
|
||||
return
|
||||
}
|
||||
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
@@ -867,10 +924,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
|
||||
task.Progress.Speed = 0
|
||||
task.timer.Start()
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
if err := task.fetcher.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := task.fetcher.Start(); err != nil {
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
d.emit(EventKeyStart, task)
|
||||
@@ -945,7 +1002,7 @@ func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||
return nil, err
|
||||
}
|
||||
fetcher := fb.Build()
|
||||
d.setupFetcher(fetcher)
|
||||
d.setupFetcher(fb, fetcher)
|
||||
return fetcher, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -342,8 +342,8 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
||||
|
||||
var httpCfg map[string]any
|
||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||
if exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
||||
if !exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
|
||||
}
|
||||
|
||||
storeCfg := &base.DownloaderStoreConfig{
|
||||
|
||||
@@ -238,7 +238,7 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
||||
select {
|
||||
case err = <-errCh:
|
||||
break
|
||||
case <-time.After(time.Second * 30):
|
||||
case <-time.After(time.Second * 10):
|
||||
err = errors.New("timeout")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ type ResolveResult struct {
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Protocol string `json:"protocol"`
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Protocol string `json:"protocol"`
|
||||
Uploading bool `json:"uploading"`
|
||||
Progress *Progress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
@@ -55,8 +56,10 @@ func (t *Task) updateStatus(status base.Status) {
|
||||
func (t *Task) clone() *Task {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
Protocol: t.Protocol,
|
||||
Meta: t.Meta,
|
||||
Status: t.Status,
|
||||
Uploading: t.Uploading,
|
||||
Progress: t.Progress,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
|
||||
@@ -2,7 +2,6 @@ gopeed.events.onError(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
gopeed.logger.info("error", ctx.error);
|
||||
ctx.task.meta.req.url = "https://github.com";
|
||||
ctx.task.pause();
|
||||
ctx.task.continue();
|
||||
});
|
||||
|
||||
@@ -4,11 +4,16 @@ type ReqExtra struct {
|
||||
Trackers []string `json:"trackers"`
|
||||
}
|
||||
|
||||
// Stats for download
|
||||
// Stats for torrent
|
||||
type Stats struct {
|
||||
// bt stats
|
||||
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||
TotalPeers int `json:"totalPeers"`
|
||||
ActivePeers int `json:"activePeers"`
|
||||
ConnectedSeeders int `json:"connectedSeeders"`
|
||||
// Total seed bytes
|
||||
SeedBytes int64 `json:"seedBytes"`
|
||||
// Seed ratio
|
||||
SeedRatio float64 `json:"seedRatio"`
|
||||
// Total seed time
|
||||
SeedTime int64 `json:"seedTime"`
|
||||
}
|
||||
@@ -41,9 +41,8 @@ class HttpConfig {
|
||||
bool useServerCtime;
|
||||
|
||||
HttpConfig({
|
||||
this.userAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
||||
this.connections = 16,
|
||||
this.userAgent = '',
|
||||
this.connections = 0,
|
||||
this.useServerCtime = false,
|
||||
});
|
||||
|
||||
@@ -55,10 +54,19 @@ class HttpConfig {
|
||||
|
||||
@JsonSerializable()
|
||||
class BtConfig {
|
||||
int listenPort = 0;
|
||||
List<String> trackers = [];
|
||||
int listenPort;
|
||||
List<String> trackers;
|
||||
bool seedKeep;
|
||||
double seedRatio;
|
||||
int seedTime;
|
||||
|
||||
BtConfig();
|
||||
BtConfig({
|
||||
this.listenPort = 0,
|
||||
this.trackers = const [],
|
||||
this.seedKeep = false,
|
||||
this.seedRatio = 0,
|
||||
this.seedTime = 0,
|
||||
});
|
||||
|
||||
factory BtConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$BtConfigFromJson(json);
|
||||
|
||||
@@ -37,9 +37,8 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
||||
};
|
||||
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||
userAgent: json['userAgent'] as String? ??
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
||||
connections: json['connections'] as int? ?? 16,
|
||||
userAgent: json['userAgent'] as String? ?? '',
|
||||
connections: json['connections'] as int? ?? 0,
|
||||
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
||||
);
|
||||
|
||||
@@ -50,14 +49,23 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
||||
'useServerCtime': instance.useServerCtime,
|
||||
};
|
||||
|
||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
||||
..listenPort = json['listenPort'] as int
|
||||
..trackers =
|
||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
|
||||
listenPort: json['listenPort'] as int? ?? 0,
|
||||
trackers: (json['trackers'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
seedKeep: json['seedKeep'] as bool? ?? false,
|
||||
seedRatio: (json['seedRatio'] as num?)?.toDouble() ?? 0,
|
||||
seedTime: json['seedTime'] as int? ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||
'listenPort': instance.listenPort,
|
||||
'trackers': instance.trackers,
|
||||
'seedKeep': instance.seedKeep,
|
||||
'seedRatio': instance.seedRatio,
|
||||
'seedTime': instance.seedTime,
|
||||
};
|
||||
|
||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
|
||||
@@ -6,11 +6,15 @@ part 'task.g.dart';
|
||||
|
||||
enum Status { ready, running, pause, wait, error, done }
|
||||
|
||||
enum Protocol { http, bt }
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Task {
|
||||
String id;
|
||||
Protocol? protocol;
|
||||
Meta meta;
|
||||
Status status;
|
||||
bool uploading;
|
||||
Progress progress;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
@@ -19,6 +23,7 @@ class Task {
|
||||
required this.id,
|
||||
required this.meta,
|
||||
required this.status,
|
||||
required this.uploading,
|
||||
required this.progress,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@@ -34,11 +39,15 @@ class Progress {
|
||||
int used;
|
||||
int speed;
|
||||
int downloaded;
|
||||
int uploadSpeed;
|
||||
int uploaded;
|
||||
|
||||
Progress({
|
||||
required this.used,
|
||||
required this.speed,
|
||||
required this.downloaded,
|
||||
required this.uploadSpeed,
|
||||
required this.uploaded,
|
||||
});
|
||||
|
||||
factory Progress.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -10,19 +10,32 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
id: json['id'] as String,
|
||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||
uploading: json['uploading'] as bool,
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
)..protocol = $enumDecodeNullable(_$ProtocolEnumMap, json['protocol']);
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'meta': instance.meta.toJson(),
|
||||
'status': _$StatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress.toJson(),
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('protocol', _$ProtocolEnumMap[instance.protocol]);
|
||||
val['meta'] = instance.meta.toJson();
|
||||
val['status'] = _$StatusEnumMap[instance.status]!;
|
||||
val['uploading'] = instance.uploading;
|
||||
val['progress'] = instance.progress.toJson();
|
||||
val['createdAt'] = instance.createdAt.toIso8601String();
|
||||
val['updatedAt'] = instance.updatedAt.toIso8601String();
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$StatusEnumMap = {
|
||||
Status.ready: 'ready',
|
||||
@@ -33,14 +46,23 @@ const _$StatusEnumMap = {
|
||||
Status.done: 'done',
|
||||
};
|
||||
|
||||
const _$ProtocolEnumMap = {
|
||||
Protocol.http: 'http',
|
||||
Protocol.bt: 'bt',
|
||||
};
|
||||
|
||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||
used: json['used'] as int,
|
||||
speed: json['speed'] as int,
|
||||
downloaded: json['downloaded'] as int,
|
||||
uploadSpeed: json['uploadSpeed'] as int,
|
||||
uploaded: json['uploaded'] as int,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
|
||||
'used': instance.used,
|
||||
'speed': instance.speed,
|
||||
'downloaded': instance.downloaded,
|
||||
'uploadSpeed': instance.uploadSpeed,
|
||||
'uploaded': instance.uploaded,
|
||||
};
|
||||
@@ -334,6 +334,69 @@ class SettingView extends GetView<SettingController> {
|
||||
},
|
||||
);
|
||||
});
|
||||
final buildBtSeedConfig = _buildConfigItem('seedConfig',
|
||||
() => 'seedKeep'.tr + (btConfig.seedKeep ? 'on'.tr : 'off'.tr),
|
||||
(Key key) {
|
||||
final seedRatioController =
|
||||
TextEditingController(text: btConfig.seedRatio.toString());
|
||||
seedRatioController.addListener(() {
|
||||
if (seedRatioController.text.isNotEmpty) {
|
||||
btConfig.seedRatio = double.parse(seedRatioController.text);
|
||||
debounceSave();
|
||||
}
|
||||
});
|
||||
final seedTimeController =
|
||||
TextEditingController(text: (btConfig.seedTime ~/ 60).toString());
|
||||
seedTimeController.addListener(() {
|
||||
if (seedTimeController.text.isNotEmpty) {
|
||||
btConfig.seedTime = int.parse(seedTimeController.text) * 60;
|
||||
debounceSave();
|
||||
}
|
||||
});
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: btConfig.seedKeep,
|
||||
onChanged: (bool value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.protocolConfig.bt.seedKeep = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
title: Text('seedKeep'.tr)),
|
||||
btConfig.seedKeep
|
||||
? null
|
||||
: TextField(
|
||||
controller: seedRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'seedRatio'.tr,
|
||||
),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d+\.?\d{0,2}')),
|
||||
],
|
||||
),
|
||||
btConfig.seedKeep
|
||||
? null
|
||||
: TextField(
|
||||
controller: seedTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'seedTime'.tr,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 100000000),
|
||||
],
|
||||
),
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
);
|
||||
});
|
||||
|
||||
// ui config items start
|
||||
final buildTheme = _buildConfigItem(
|
||||
@@ -833,6 +896,7 @@ class SettingView extends GetView<SettingController> {
|
||||
buildBtListenPort(),
|
||||
buildBtTrackerSubscribeUrls(),
|
||||
buildBtTrackers(),
|
||||
buildBtSeedConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
|
||||
@@ -95,5 +95,9 @@ const enUS = {
|
||||
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
|
||||
'browserExtension': 'Browser Extension',
|
||||
'launchAtStartup': 'Launch at Startup',
|
||||
'seedConfig': 'Seed Config',
|
||||
'seedKeep': 'Keep seeding',
|
||||
'seedRatio': 'Seed ratio',
|
||||
'seedTime': 'Seed time (minutes)',
|
||||
},
|
||||
};
|
||||
@@ -92,5 +92,9 @@ const zhCN = {
|
||||
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
|
||||
'browserExtension': '浏览器扩展',
|
||||
'launchAtStartup': '开机自动运行',
|
||||
'seedConfig': '做种设置',
|
||||
'seedKeep': '持续做种',
|
||||
'seedRatio': '做种分享率',
|
||||
'seedTime': '做种时间(分钟)',
|
||||
}
|
||||
};
|
||||
@@ -19,8 +19,7 @@ const zhTW = {
|
||||
'advancedOptions': '進階選項',
|
||||
'downloadLink': '下載連結',
|
||||
'downloadLinkValid': '請輸入下載連結',
|
||||
'downloadLinkHit':
|
||||
'請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
||||
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
||||
'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處',
|
||||
'download': '下載',
|
||||
'noFileSelected': '請至少選擇一個檔案以繼續。',
|
||||
@@ -93,5 +92,9 @@ const zhTW = {
|
||||
'thanksDesc': '感謝所有幫助建立和發展 Gopeed 社群的貢獻者!',
|
||||
'browserExtension': '瀏覽器擴充功能',
|
||||
'launchAtStartup': '開機自動執行',
|
||||
'seedConfig': '做種設定',
|
||||
'seedKeep': '持續做種',
|
||||
'seedRatio': '做種分享率',
|
||||
'seedTime': '做種時間(分鐘)',
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user