feat: torrent seed config (#599)

This commit is contained in:
Levi authored and GitHub committed 2024-07-30 12:50:06 +08:00
1 parent 473c58796c
commit 3b50465708
22 files changed
+456 -144

No files matched your search

+2 -4
View File
@@ -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
},
}
}
+11 -4
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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),
}
}
}
+9 -7
View File
@@ -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)
+12 -11
View File
@@ -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{
+9 -7
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
}
+2 -2
View File
@@ -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{
+1 -1
View File
@@ -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")
}
+4 -1
View File
@@ -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,
-1
View File
@@ -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();
});
+7 -2
View File
@@ -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(
+9
View File
@@ -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) =>
+31 -9
View File
@@ -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),
+4
View File
@@ -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)',
},
};
+4
View File
@@ -92,5 +92,9 @@ const zhCN = {
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
'browserExtension': '浏览器扩展',
'launchAtStartup': '开机自动运行',
'seedConfig': '做种设置',
'seedKeep': '持续做种',
'seedRatio': '做种分享率',
'seedTime': '做种时间(分钟)',
}
};
+5 -2
View File
@@ -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': '做種時間(分鐘)',
}
};