From 9e7cbed882b00dd5839b1c98874c4a347a052421 Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 17 Mar 2026 17:44:18 +0800 Subject: [PATCH 1/3] feat: ed2k protocol support --- go.mod | 1 + go.sum | 2 + internal/fetcher/fetcher.go | 15 + internal/protocol/ed2k/config.go | 15 + internal/protocol/ed2k/fetcher.go | 505 ++++++++++++++++++ internal/protocol/ed2k/fetcher_test.go | 42 ++ pkg/download/downloader.go | 53 +- pkg/download/model.go | 10 + pkg/download/model_test.go | 18 + pkg/protocol/ed2k/model.go | 14 + .../lib/api/model/downloader_config.dart | 23 + .../lib/api/model/downloader_config.g.dart | 22 +- ui/flutter/lib/api/model/task.dart | 2 +- ui/flutter/lib/api/model/task.g.dart | 1 + .../app/modules/create/views/create_view.dart | 10 +- .../modules/setting/views/setting_view.dart | 149 +++++- ui/flutter/lib/i18n/langs/ca_es.dart | 50 +- ui/flutter/lib/i18n/langs/de_de.dart | 11 + ui/flutter/lib/i18n/langs/en_us.dart | 11 + ui/flutter/lib/i18n/langs/es_es.dart | 11 + ui/flutter/lib/i18n/langs/fa_ir.dart | 11 + ui/flutter/lib/i18n/langs/fr_fr.dart | 11 + ui/flutter/lib/i18n/langs/hu_hu.dart | 11 + ui/flutter/lib/i18n/langs/id_id.dart | 11 + ui/flutter/lib/i18n/langs/it_it.dart | 11 + ui/flutter/lib/i18n/langs/ja_jp.dart | 11 + ui/flutter/lib/i18n/langs/pl_pl.dart | 11 + ui/flutter/lib/i18n/langs/pt_br.dart | 11 + ui/flutter/lib/i18n/langs/ru_ru.dart | 11 + ui/flutter/lib/i18n/langs/ta_ta.dart | 11 + ui/flutter/lib/i18n/langs/tr_tr.dart | 43 +- ui/flutter/lib/i18n/langs/uk_ua.dart | 11 + ui/flutter/lib/i18n/langs/vi_vn.dart | 11 + ui/flutter/lib/i18n/langs/zh_cn.dart | 11 + ui/flutter/lib/i18n/langs/zh_tw.dart | 11 + 35 files changed, 1127 insertions(+), 35 deletions(-) create mode 100644 internal/protocol/ed2k/config.go create mode 100644 internal/protocol/ed2k/fetcher.go create mode 100644 internal/protocol/ed2k/fetcher_test.go create mode 100644 pkg/download/model_test.go create mode 100644 pkg/protocol/ed2k/model.go diff --git a/go.mod b/go.mod index 10b7c2e5d..ed48d0fc1 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/klauspost/pgzip v1.2.6 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect + github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect diff --git a/go.sum b/go.sum index c35b44f47..d0c935b16 100644 --- a/go.sum +++ b/go.sum @@ -386,6 +386,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 h1:tEOucyKJzFsCz5Gr41jFHj8i2g1zemTyS4uyErJgFHc= +github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= diff --git a/internal/fetcher/fetcher.go b/internal/fetcher/fetcher.go index f4a79ac27..5e946abe7 100644 --- a/internal/fetcher/fetcher.go +++ b/internal/fetcher/fetcher.go @@ -127,6 +127,21 @@ type FetcherManager interface { Close() error } +// StatefulFetcherManager is an optional extension for protocols that keep +// shared client state outside individual task fetchers. +type StatefulFetcherManager interface { + SetStateStore(store ProtocolStateStore) +} + +// ProtocolStateStore persists shared protocol state for a fetcher manager. +// Downloader provides the concrete storage backend, while the protocol decides +// when state should be loaded or flushed. +type ProtocolStateStore interface { + Load(v any) (bool, error) + Save(v any) error + Delete() error +} + type DefaultFetcher struct { Ctl *controller.Controller Meta *FetcherMeta diff --git a/internal/protocol/ed2k/config.go b/internal/protocol/ed2k/config.go new file mode 100644 index 000000000..9eb8416e5 --- /dev/null +++ b/internal/protocol/ed2k/config.go @@ -0,0 +1,15 @@ +package ed2k + +const ( + defaultServerList = "45.82.80.155:5687,176.123.5.89:4725,85.121.5.137:4232,176.123.2.239:4232,145.239.2.134:4661,91.208.162.87:4232,37.15.61.236:4232" + defaultServerMet = "ed2k://|serverlist|http://upd.emule-security.org/server.met|/" + defaultNodesDat = "https://upd.emule-security.org/nodes.dat" +) + +type config struct { + ListenPort int `json:"listenPort"` + UDPPort int `json:"udpPort"` + ServerAddr string `json:"serverAddr"` + ServerMet string `json:"serverMet"` + NodesDat string `json:"nodesDat"` +} diff --git a/internal/protocol/ed2k/fetcher.go b/internal/protocol/ed2k/fetcher.go new file mode 100644 index 000000000..46c86b62b --- /dev/null +++ b/internal/protocol/ed2k/fetcher.go @@ -0,0 +1,505 @@ +package ed2k + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/GopeedLab/gopeed/internal/controller" + "github.com/GopeedLab/gopeed/internal/fetcher" + "github.com/GopeedLab/gopeed/pkg/base" + ped2k "github.com/GopeedLab/gopeed/pkg/protocol/ed2k" + "github.com/monkeyWie/goed2k" + gprotocol "github.com/monkeyWie/goed2k/protocol" +) + +type clientStateStore struct { + store fetcher.ProtocolStateStore +} + +func (s *clientStateStore) Load() (*goed2k.ClientState, error) { + if s == nil || s.store == nil { + return nil, nil + } + var state goed2k.ClientState + exist, err := s.store.Load(&state) + if err != nil { + return nil, err + } + if !exist { + return nil, nil + } + return &state, nil +} + +func (s *clientStateStore) Save(state *goed2k.ClientState) error { + if s == nil || s.store == nil { + return nil + } + if state == nil { + return s.store.Delete() + } + return s.store.Save(state) +} + +type Fetcher struct { + ctl *controller.Controller + config *config + + manager *FetcherManager + meta *fetcher.FetcherMeta + handle goed2k.TransferHandle + + waitCtx context.Context + waitCancel context.CancelFunc +} + +func (f *Fetcher) Setup(ctl *controller.Controller) { + f.ctl = ctl + if f.meta == nil { + f.meta = &fetcher.FetcherMeta{} + } + f.waitCtx, f.waitCancel = context.WithCancel(context.Background()) + f.ctl.GetConfig(&f.config) +} + +func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error { + link, err := parseLink(req.URL) + if err != nil { + return err + } + + f.meta.Req = req + f.meta.Opts = opts + if f.meta.Opts == nil { + f.meta.Opts = &base.Options{} + } + f.meta.Res = buildResource(link) + return nil +} + +func (f *Fetcher) Start() error { + link, err := parseLink(f.meta.Req.URL) + if err != nil { + return err + } + if f.meta.Res == nil { + f.meta.Res = buildResource(link) + } + + client, err := f.getClient() + if err != nil { + return err + } + + targetPath := f.meta.SingleFilepath() + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return err + } + + handle := client.FindTransfer(link.Hash) + if handle.IsValid() { + f.handle = handle + if handle.IsPaused() { + if err := client.ResumeTransfer(handle.GetHash()); err != nil { + return err + } + } + return nil + } + + atp := goed2k.AddTransferParams{ + Hash: link.Hash, + CreateTime: time.Now().UnixMilli(), + Size: link.NumberValue, + FilePath: targetPath, + } + handle, err = client.AddTransfer(atp) + if err != nil { + return err + } + f.handle = handle + if handle.IsValid() && handle.IsPaused() { + if err := client.ResumeTransfer(handle.GetHash()); err != nil { + return err + } + } + return nil +} + +func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error { + handle := f.currentHandle() + + if opts != nil && (opts.Name != "" || opts.Path != "") && handle.IsValid() { + return errors.New("cannot change ed2k target path after transfer started") + } + if req != nil && req.URL != "" && handle.IsValid() { + return errors.New("cannot change ed2k link after transfer started") + } + + if req != nil { + if req.URL != "" { + link, err := parseLink(req.URL) + if err != nil { + return err + } + f.meta.Req.URL = req.URL + f.meta.Res = buildResource(link) + } + if req.Labels != nil { + if f.meta.Req.Labels == nil { + f.meta.Req.Labels = make(map[string]string) + } + for k, v := range req.Labels { + f.meta.Req.Labels[k] = v + } + } + if req.Proxy != nil { + f.meta.Req.Proxy = req.Proxy + } + } + + if opts != nil { + if opts.Name != "" { + f.meta.Opts.Name = opts.Name + } + if opts.Path != "" { + f.meta.Opts.Path = opts.Path + } + } + return nil +} + +func (f *Fetcher) Pause() error { + handle := f.currentHandle() + if !handle.IsValid() { + return nil + } + client, err := f.getClient() + if err != nil { + return err + } + if err := client.PauseTransfer(handle.GetHash()); err != nil { + return err + } + f.handle = handle + return nil +} + +func (f *Fetcher) Close() error { + if f.waitCancel != nil { + f.waitCancel() + } + handle := f.currentHandle() + if !handle.IsValid() { + return nil + } + client, err := f.getClient() + if err != nil { + return err + } + f.handle = handle + return client.RemoveTransfer(handle.GetHash(), false) +} + +func (f *Fetcher) Meta() *fetcher.FetcherMeta { + return f.meta +} + +func (f *Fetcher) Stats() any { + handle := f.currentHandle() + if !handle.IsValid() { + return &ped2k.Stats{} + } + status := handle.GetStatus() + return &ped2k.Stats{ + State: string(status.State), + ActivePeers: handle.ActiveConnections(), + TotalPeers: status.NumPeers, + DownloadRate: status.DownloadRate, + Upload: status.Upload, + UploadRate: status.UploadRate, + TotalDone: status.TotalDone, + TotalReceived: status.TotalReceived, + TotalWanted: status.TotalWanted, + } +} + +func (f *Fetcher) Progress() fetcher.Progress { + handle := f.currentHandle() + if !handle.IsValid() { + return fetcher.Progress{0} + } + return fetcher.Progress{handle.GetStatus().TotalReceived} +} + +func (f *Fetcher) Wait() error { + client, err := f.getClient() + if err != nil { + return err + } + + hash, err := f.hash() + if err != nil { + return err + } + + handle := f.currentHandle() + if handle.IsValid() && handle.IsFinished() { + return nil + } + + progressCh, cancel := client.SubscribeTransferProgress() + defer cancel() + + for { + select { + case <-f.waitCtx.Done(): + return nil + case event, ok := <-progressCh: + if !ok { + return nil + } + for _, transfer := range event.Transfers { + if transfer.Hash.Compare(hash) != 0 { + continue + } + // Removal can happen during task deletion or client shutdown, both of + // which should unblock Wait without treating it as a download failure. + if transfer.Removed || transfer.State == goed2k.Finished { + return nil + } + } + } + } +} + +func (f *Fetcher) getClient() (*goed2k.Client, error) { + if f.manager == nil { + f.manager = &FetcherManager{} + } + return f.manager.initClient(f.config) +} + +func (f *Fetcher) currentHandle() goed2k.TransferHandle { + if f.handle.IsValid() { + return f.handle + } + if f.manager == nil { + return f.handle + } + client := f.manager.currentClient() + if client == nil { + return f.handle + } + hash, err := f.hash() + if err != nil { + return f.handle + } + handle := client.FindTransfer(hash) + if handle.IsValid() { + f.handle = handle + } + return f.handle +} + +func (f *Fetcher) hash() (gprotocol.Hash, error) { + if f.meta == nil || f.meta.Req == nil { + return gprotocol.Invalid, errors.New("ed2k link is empty") + } + link, err := parseLink(f.meta.Req.URL) + if err != nil { + return gprotocol.Invalid, err + } + return link.Hash, nil +} + +type FetcherManager struct { + mu sync.Mutex + client *goed2k.Client + stateStore *clientStateStore +} + +func (fm *FetcherManager) SetStateStore(store fetcher.ProtocolStateStore) { + fm.mu.Lock() + defer fm.mu.Unlock() + + if fm.stateStore == nil { + fm.stateStore = &clientStateStore{} + } + fm.stateStore.store = store + if fm.client != nil { + fm.client.SetStateStore(fm.stateStore) + } +} + +func (fm *FetcherManager) Name() string { + return "ed2k" +} + +func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter { + return []*fetcher.SchemeFilter{ + { + Type: fetcher.FilterTypeUrl, + Pattern: "ED2K", + }, + } +} + +func (fm *FetcherManager) Build() fetcher.Fetcher { + return &Fetcher{manager: fm} +} + +func (fm *FetcherManager) ParseName(u string) string { + link, err := parseLink(u) + if err != nil { + return "" + } + return link.StringValue +} + +func (fm *FetcherManager) AutoRename() bool { + return true +} + +func (fm *FetcherManager) DefaultConfig() any { + return &config{ + ListenPort: 0, + UDPPort: 0, + ServerAddr: defaultServerList, + ServerMet: defaultServerMet, + NodesDat: defaultNodesDat, + } +} + +func (fm *FetcherManager) Store(f fetcher.Fetcher) (any, error) { + return nil, nil +} + +func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) { + return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher { + return &Fetcher{ + manager: fm, + meta: meta, + } + } +} + +func (fm *FetcherManager) Close() error { + fm.mu.Lock() + defer fm.mu.Unlock() + + if fm.client != nil { + fm.client.Close() + fm.client = nil + } + return nil +} + +func parseLink(raw string) (goed2k.EMuleLink, error) { + link, err := goed2k.ParseEMuleLink(raw) + if err != nil { + return goed2k.EMuleLink{}, err + } + if link.Type != goed2k.LinkFile { + return goed2k.EMuleLink{}, errors.New("unsupported ed2k link type") + } + return link, nil +} + +func buildResource(link goed2k.EMuleLink) *base.Resource { + return &base.Resource{ + Size: link.NumberValue, + Range: false, + Hash: link.Hash.String(), + Files: []*base.FileInfo{ + { + Name: link.StringValue, + Size: link.NumberValue, + }, + }, + } +} + +func splitCommaList(value string) []string { + if strings.TrimSpace(value) == "" { + return nil + } + parts := strings.Split(value, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func (fm *FetcherManager) getStateStoreLocked() *clientStateStore { + if fm.stateStore == nil { + fm.stateStore = &clientStateStore{} + } + return fm.stateStore +} + +func (fm *FetcherManager) currentClient() *goed2k.Client { + fm.mu.Lock() + defer fm.mu.Unlock() + return fm.client +} + +func (fm *FetcherManager) initClient(cfg *config) (*goed2k.Client, error) { + fm.mu.Lock() + defer fm.mu.Unlock() + + if fm.client != nil { + return fm.client, nil + } + if cfg == nil { + cfg = fm.DefaultConfig().(*config) + } + + settings := goed2k.NewSettings() + settings.ListenPort = cfg.ListenPort + settings.UDPPort = cfg.UDPPort + settings.EnableDHT = true + settings.EnableUPnP = true + settings.ReconnectToServer = true + + client := goed2k.NewClient(settings) + client.SetStateStore(fm.getStateStoreLocked()) + if err := client.LoadState(""); err != nil { + return nil, err + } + if err := client.Start(); err != nil { + return nil, err + } + fm.client = client + // Bootstrap is best-effort: downloads can still proceed later even if + // server list or DHT initialization fails during startup. + bootstrapClient(client, cfg) + return fm.client, nil +} + +func bootstrapClient(client *goed2k.Client, cfg *config) { + for _, serverAddr := range splitCommaList(cfg.ServerAddr) { + go func(serverAddr string) { + _ = client.Connect(serverAddr) + }(serverAddr) + } + for _, source := range splitCommaList(cfg.ServerMet) { + go func(source string) { + _ = client.ConnectServerMet(source) + }(source) + } + if cfg.NodesDat != "" { + go func() { + _ = client.LoadDHTNodesDat(cfg.NodesDat) + }() + } +} diff --git a/internal/protocol/ed2k/fetcher_test.go b/internal/protocol/ed2k/fetcher_test.go new file mode 100644 index 000000000..16dcd79ee --- /dev/null +++ b/internal/protocol/ed2k/fetcher_test.go @@ -0,0 +1,42 @@ +package ed2k + +import ( + "testing" + + "github.com/GopeedLab/gopeed/internal/controller" + "github.com/GopeedLab/gopeed/pkg/base" +) + +const testLink = "ed2k://|file|Taylor,%20Elizabeth%20-%20Prohibido%20morir%20aqui%20[66672]%20(r1.0).epub|434885|23A8CEFF57A7A32D562D649ED7893796|/" + +func TestFetcher_Resolve(t *testing.T) { + f := (&FetcherManager{}).Build() + f.Setup(controller.NewController()) + + err := f.Resolve(&base.Request{URL: testLink}, &base.Options{Path: t.TempDir()}) + if err != nil { + t.Fatalf("Resolve() error = %v", err) + } + + meta := f.Meta() + if meta.Res == nil { + t.Fatal("Resolve() resource is nil") + } + if got, want := meta.Res.Hash, "23A8CEFF57A7A32D562D649ED7893796"; got != want { + t.Fatalf("Resolve() hash = %s, want %s", got, want) + } + if got, want := meta.Res.Size, int64(434885); got != want { + t.Fatalf("Resolve() size = %d, want %d", got, want) + } + if got, want := meta.Res.Files[0].Name, "Taylor, Elizabeth - Prohibido morir aqui [66672] (r1.0).epub"; got != want { + t.Fatalf("Resolve() name = %q, want %q", got, want) + } +} + +func TestFetcherManager_ParseName(t *testing.T) { + got := (&FetcherManager{}).ParseName(testLink) + want := "Taylor, Elizabeth - Prohibido morir aqui [66672] (r1.0).epub" + if got != want { + t.Fatalf("ParseName() = %q, want %q", got, want) + } +} diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 6dbf9169c..15a0e6c4e 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -31,6 +31,8 @@ const ( bucketTask = "task" // task download data bucket bucketSave = "save" + // protocol-level shared client state bucket + bucketProtocolState = "protocol_state" // downloader config bucket bucketConfig = "config" // downloader extension bucket @@ -141,7 +143,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader { func (d *Downloader) Setup() error { // setup storage - if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil { + if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketProtocolState, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil { return err } // load config from storage @@ -165,6 +167,12 @@ func (d *Downloader) Setup() error { if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok { d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fm.DefaultConfig() } + if sfm, ok := fm.(fetcher.StatefulFetcherManager); ok { + sfm.SetStateStore(&protocolStateStore{ + storage: d.storage, + protocol: protocol, + }) + } } // load tasks from storage @@ -358,9 +366,16 @@ func (d *Downloader) saveTask(task *Task) error { d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID) return err } - if err := d.storage.Put(bucketSave, task.ID, data); err != nil { - d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID) - return err + if data != nil { + if err := d.storage.Put(bucketSave, task.ID, data); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID) + return err + } + } else { + if err := d.storage.Delete(bucketSave, task.ID); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("clear fetcher state failed: %s", task.ID) + return err + } } if err := d.storage.Put(bucketTask, task.ID, task); err != nil { d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID) @@ -820,6 +835,26 @@ func (d *Downloader) Clear() error { return nil } +type protocolStateStore struct { + storage Storage + protocol string +} + +func (s *protocolStateStore) Load(v any) (bool, error) { + return s.storage.Get(bucketProtocolState, s.protocol, v) +} + +func (s *protocolStateStore) Save(v any) error { + if v == nil { + return s.Delete() + } + return s.storage.Put(bucketProtocolState, s.protocol, v) +} + +func (s *protocolStateStore) Delete() error { + return s.storage.Delete(bucketProtocolState, s.protocol) +} + func (d *Downloader) Listener(fn Listener) { d.listener = fn } @@ -1300,8 +1335,14 @@ func (d *Downloader) doPause(task *Task) (err error) { return err } } - if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { - return err + if task.fetcherManager != nil && task.fetcher != nil { + if err := d.saveTask(task); err != nil { + return err + } + } else { + if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { + return err + } } d.emit(EventKeyPause, task) return nil diff --git a/pkg/download/model.go b/pkg/download/model.go index c3126c1ce..0d3ce16e3 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -8,6 +8,7 @@ import ( "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/protocol/bt" + "github.com/GopeedLab/gopeed/internal/protocol/ed2k" "github.com/GopeedLab/gopeed/internal/protocol/http" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/util" @@ -110,6 +111,14 @@ func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 { } func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 { + if usedTime <= 0 { + return 0 + } + if downloaded < 0 { + *speedArr = (*speedArr)[:0] + return 0 + } + *speedArr = append(*speedArr, downloaded) // Record last 5 seconds of download speed to calculate the average speed if len(*speedArr) > int(5.0/usedTime) { @@ -156,6 +165,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig { cfg.FetchManagers = []fetcher.FetcherManager{ new(http.FetcherManager), new(bt.FetcherManager), + new(ed2k.FetcherManager), } } if cfg.RefreshInterval == 0 { diff --git a/pkg/download/model_test.go b/pkg/download/model_test.go new file mode 100644 index 000000000..98da0cb49 --- /dev/null +++ b/pkg/download/model_test.go @@ -0,0 +1,18 @@ +package download + +import "testing" + +func TestCalcSpeedResetOnRollback(t *testing.T) { + speedArr := []int64{1024, 2048, 4096} + + if got := calcSpeed(&speedArr, -512, 1); got != 0 { + t.Fatalf("calcSpeed() = %d, want 0 after rollback", got) + } + if len(speedArr) != 0 { + t.Fatalf("speed window len = %d, want 0 after rollback", len(speedArr)) + } + + if got := calcSpeed(&speedArr, 1024, 1); got != 1024 { + t.Fatalf("calcSpeed() = %d, want 1024 after reset", got) + } +} diff --git a/pkg/protocol/ed2k/model.go b/pkg/protocol/ed2k/model.go new file mode 100644 index 000000000..226a39bdc --- /dev/null +++ b/pkg/protocol/ed2k/model.go @@ -0,0 +1,14 @@ +package ed2k + +type Stats struct { + State string `json:"state"` + Paused bool `json:"paused"` + ActivePeers int `json:"activePeers"` + TotalPeers int `json:"totalPeers"` + DownloadRate int `json:"downloadRate"` + Upload int64 `json:"upload"` + UploadRate int `json:"uploadRate"` + TotalDone int64 `json:"totalDone"` + TotalReceived int64 `json:"totalReceived"` + TotalWanted int64 `json:"totalWanted"` +} diff --git a/ui/flutter/lib/api/model/downloader_config.dart b/ui/flutter/lib/api/model/downloader_config.dart index d481f89c7..69f02027f 100644 --- a/ui/flutter/lib/api/model/downloader_config.dart +++ b/ui/flutter/lib/api/model/downloader_config.dart @@ -31,6 +31,7 @@ class DownloaderConfig { class ProtocolConfig { HttpConfig http = HttpConfig(); BtConfig bt = BtConfig(); + Ed2kConfig ed2k = Ed2kConfig(); ProtocolConfig(); @@ -80,6 +81,28 @@ class BtConfig { Map toJson() => _$BtConfigToJson(this); } +@JsonSerializable() +class Ed2kConfig { + int listenPort; + int udpPort; + String serverAddr; + String serverMet; + String nodesDat; + + Ed2kConfig({ + this.listenPort = 0, + this.udpPort = 0, + this.serverAddr = '', + this.serverMet = '', + this.nodesDat = '', + }); + + factory Ed2kConfig.fromJson(Map json) => + _$Ed2kConfigFromJson(json); + + Map toJson() => _$Ed2kConfigToJson(this); +} + @JsonSerializable(explicitToJson: true) class ExtraConfig { String themeMode; diff --git a/ui/flutter/lib/api/model/downloader_config.g.dart b/ui/flutter/lib/api/model/downloader_config.g.dart index 01c9aa433..8284efedf 100644 --- a/ui/flutter/lib/api/model/downloader_config.g.dart +++ b/ui/flutter/lib/api/model/downloader_config.g.dart @@ -42,12 +42,15 @@ Map _$DownloaderConfigToJson(DownloaderConfig instance) => ProtocolConfig _$ProtocolConfigFromJson(Map json) => ProtocolConfig() ..http = HttpConfig.fromJson(json['http'] as Map) - ..bt = BtConfig.fromJson(json['bt'] as Map); + ..bt = BtConfig.fromJson(json['bt'] as Map) + ..ed2k = Ed2kConfig.fromJson( + json['ed2k'] as Map? ?? {}); Map _$ProtocolConfigToJson(ProtocolConfig instance) => { 'http': instance.http.toJson(), 'bt': instance.bt.toJson(), + 'ed2k': instance.ed2k.toJson(), }; HttpConfig _$HttpConfigFromJson(Map json) => HttpConfig( @@ -82,6 +85,23 @@ Map _$BtConfigToJson(BtConfig instance) => { 'seedTime': instance.seedTime, }; +Ed2kConfig _$Ed2kConfigFromJson(Map json) => Ed2kConfig( + listenPort: (json['listenPort'] as num?)?.toInt() ?? 0, + udpPort: (json['udpPort'] as num?)?.toInt() ?? 0, + serverAddr: json['serverAddr'] as String? ?? '', + serverMet: json['serverMet'] as String? ?? '', + nodesDat: json['nodesDat'] as String? ?? '', + ); + +Map _$Ed2kConfigToJson(Ed2kConfig instance) => + { + 'listenPort': instance.listenPort, + 'udpPort': instance.udpPort, + 'serverAddr': instance.serverAddr, + 'serverMet': instance.serverMet, + 'nodesDat': instance.nodesDat, + }; + ExtraConfig _$ExtraConfigFromJson(Map json) => ExtraConfig( themeMode: json['themeMode'] as String? ?? '', locale: json['locale'] as String? ?? '', diff --git a/ui/flutter/lib/api/model/task.dart b/ui/flutter/lib/api/model/task.dart index d9c0f1fc0..830d772dd 100644 --- a/ui/flutter/lib/api/model/task.dart +++ b/ui/flutter/lib/api/model/task.dart @@ -6,7 +6,7 @@ part 'task.g.dart'; enum Status { ready, running, pause, wait, error, done } -enum Protocol { http, bt } +enum Protocol { http, bt, ed2k } // ExtractStatus enum matching Go backend enum ExtractStatus { diff --git a/ui/flutter/lib/api/model/task.g.dart b/ui/flutter/lib/api/model/task.g.dart index 4b65632c4..b2889e73b 100644 --- a/ui/flutter/lib/api/model/task.g.dart +++ b/ui/flutter/lib/api/model/task.g.dart @@ -51,6 +51,7 @@ const _$StatusEnumMap = { const _$ProtocolEnumMap = { Protocol.http: 'http', Protocol.bt: 'bt', + Protocol.ed2k: 'ed2k', }; Progress _$ProgressFromJson(Map json) => Progress( diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index fb45f91b7..b1871afda 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -59,7 +59,7 @@ class CreateView extends GetView { final _btTrackerController = TextEditingController(); final _archivePasswordController = TextEditingController(); - final _availableSchemes = ["http:", "https:", "magnet:"]; + final _availableSchemes = ["http:", "https:", "magnet:", "ed2k:"]; final _skipVerifyCertController = false.obs; final _autoTorrentController = Rxn(); @@ -116,6 +116,7 @@ class CreateView extends GetView { jsonDecode(jsonEncode(routerParams.req!.extra))); _btTrackerController.text = reqExtra.trackers.join("\n"); }, + Protocol.ed2k: null, }; if (routerParams.req?.extra != null) { extraHandlers[protocol]?.call(); @@ -139,6 +140,7 @@ class CreateView extends GetView { } }, Protocol.bt: null, + Protocol.ed2k: null, }; if (routerParams.opts?.extra != null) { optionsHandlers[protocol]?.call(); @@ -804,6 +806,9 @@ class CreateView extends GetView { uppercaseUrl.endsWith(".TORRENT")) { protocol = Protocol.bt; } + if (uppercaseUrl.startsWith("ED2K:")) { + protocol = Protocol.ed2k; + } return protocol; } @@ -1007,6 +1012,9 @@ class CreateView extends GetView { ..trackers = Util.textToLines(_btTrackerController.text); } break; + case Protocol.ed2k: + case null: + break; } return reqExtra; } diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index bfc4601d8..7644e9fa5 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -501,14 +501,16 @@ class SettingView extends GetView { final buildDesktopNotification = !Util.isDesktop() ? () => null : _buildConfigItem('desktopNotification', () { - return appController.downloaderConfig.value.extra.desktopNotification + return appController + .downloaderConfig.value.extra.desktopNotification ? 'on'.tr : 'off'.tr; }, (Key key) { return Container( alignment: Alignment.centerLeft, child: Switch( - value: appController.downloaderConfig.value.extra.desktopNotification, + value: appController + .downloaderConfig.value.extra.desktopNotification, onChanged: (bool value) async { appController.downloaderConfig.update((val) { val!.extra.desktopNotification = value; @@ -787,6 +789,138 @@ class SettingView extends GetView { ); }); + // ed2k config items start + final ed2kConfig = downloaderCfg.value.protocolConfig.ed2k; + List parseEd2kEntries(String value) { + return value + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + } + + String summarizeEd2kEntries(String value) { + final entries = parseEd2kEntries(value); + if (entries.isEmpty) { + return 'notSet'.tr; + } + return 'items'.trParams({'count': entries.length.toString()}); + } + + String formatEd2kMultilineValue(String value) { + return parseEd2kEntries(value).join('\r\n'); + } + + final buildEd2kListenPort = _buildConfigItem( + 'ed2kTcpPort', () => ed2kConfig.listenPort.toString(), (Key key) { + final controller = + TextEditingController(text: ed2kConfig.listenPort.toString()); + controller.addListener(() async { + if (controller.text.isNotEmpty && + controller.text != ed2kConfig.listenPort.toString()) { + ed2kConfig.listenPort = int.parse(controller.text); + await debounceSave(); + } + }); + + return TextField( + key: key, + focusNode: FocusNode(), + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + NumericalRangeFormatter(min: 0, max: 65535), + ], + ); + }); + final buildEd2kUdpPort = _buildConfigItem( + 'ed2kUdpPort', () => ed2kConfig.udpPort.toString(), (Key key) { + final controller = + TextEditingController(text: ed2kConfig.udpPort.toString()); + controller.addListener(() async { + if (controller.text.isNotEmpty && + controller.text != ed2kConfig.udpPort.toString()) { + ed2kConfig.udpPort = int.parse(controller.text); + await debounceSave(); + } + }); + + return TextField( + key: key, + focusNode: FocusNode(), + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + NumericalRangeFormatter(min: 0, max: 65535), + ], + ); + }); + final buildEd2kServerAddr = _buildConfigItem( + 'ed2kServerList', () => summarizeEd2kEntries(ed2kConfig.serverAddr), + (Key key) { + final controller = TextEditingController( + text: formatEd2kMultilineValue(ed2kConfig.serverAddr)); + return TextField( + key: key, + focusNode: FocusNode(), + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: 5, + decoration: InputDecoration( + hintText: 'ed2kServersHint'.tr, + helperText: 'ed2kOnePerLine'.tr, + ), + onChanged: (value) async { + ed2kConfig.serverAddr = Util.textToLines(value).join(','); + await debounceSave(); + }, + ); + }); + final buildEd2kServerMet = _buildConfigItem( + 'ed2kServerMet', () => summarizeEd2kEntries(ed2kConfig.serverMet), + (Key key) { + final controller = TextEditingController( + text: formatEd2kMultilineValue(ed2kConfig.serverMet)); + return TextField( + key: key, + focusNode: FocusNode(), + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: 4, + decoration: InputDecoration( + hintText: 'ed2kServerMetHint'.tr, + helperText: 'ed2kOnePerLine'.tr, + ), + onChanged: (value) async { + ed2kConfig.serverMet = Util.textToLines(value).join(','); + await debounceSave(); + }, + ); + }); + final buildEd2kNodesDat = _buildConfigItem( + 'ed2kNodesDat', () => summarizeEd2kEntries(ed2kConfig.nodesDat), + (Key key) { + final controller = TextEditingController( + text: formatEd2kMultilineValue(ed2kConfig.nodesDat)); + return TextField( + key: key, + focusNode: FocusNode(), + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: 4, + decoration: InputDecoration( + hintText: 'ed2kNodesDatHint'.tr, + helperText: 'ed2kOnePerLine'.tr, + ), + onChanged: (value) async { + ed2kConfig.nodesDat = Util.textToLines(value).join(','); + await debounceSave(); + }, + ); + }); + // ui config items start final buildTheme = _buildConfigItem( 'theme', @@ -1648,6 +1782,17 @@ class SettingView extends GetView { buildBtDefaultClientConfig(), ]), )), + Text('ed2k'.tr), + Card( + child: Column( + children: _addDivider([ + buildEd2kListenPort(), + buildEd2kUdpPort(), + buildEd2kServerAddr(), + buildEd2kServerMet(), + buildEd2kNodesDat(), + ]), + )), Text('ui'.tr), Card( child: Column( diff --git a/ui/flutter/lib/i18n/langs/ca_es.dart b/ui/flutter/lib/i18n/langs/ca_es.dart index a332cfe16..17f072539 100644 --- a/ui/flutter/lib/i18n/langs/ca_es.dart +++ b/ui/flutter/lib/i18n/langs/ca_es.dart @@ -35,14 +35,18 @@ const caES = { 'downloadDir': 'Carpeta de baixades', 'downloadDirValid': 'Seleccioneu la carpeta de baixades', 'connections': 'Connexions', - 'useServerCtime': 'Utilitza l\'hora del servidor per a la creació de fitxers', + 'useServerCtime': + 'Utilitza l\'hora del servidor per a la creació de fitxers', 'maxRunning': 'Màxim de tasques en execució', 'defaultDirectDownload': 'Activa la baixada directa per defecte', - 'autoStartTasks': 'Inicia automàticament les tasques incompletes a l\'inici', - 'autoTorrentEnable': 'Crea automàticament tasques BT a partir de fitxers .torrent', + 'autoStartTasks': + 'Inicia automàticament les tasques incompletes a l\'inici', + 'autoTorrentEnable': + 'Crea automàticament tasques BT a partir de fitxers .torrent', 'autoTorrentDeleteAfterDownload': 'Suprimeix el fitxer .torrent després de crear la tasca BT', - 'autoDeleteMissingFileTasks': 'Suprimeix automàticament les tasques dels fitxers que falten', + 'autoDeleteMissingFileTasks': + 'Suprimeix automàticament les tasques dels fitxers que falten', 'items': '@count elements', 'subscribeTracker': 'Subscripció al rastrejador', 'subscribeFail': @@ -51,7 +55,8 @@ const caES = { 'updateDaily': 'Actualització diària', 'lastUpdate': 'Última actualització: @time', 'addTracker': 'Afegeix un rastrejador', - 'addTrackerHit': 'Introduïu la URL del servidor del rastrejador, un per línia', + 'addTrackerHit': + 'Introduïu la URL del servidor del rastrejador, un per línia', 'ui': 'UI', 'theme': 'Tema', 'themeSystem': 'Sistema', @@ -60,7 +65,8 @@ const caES = { 'locale': 'Idioma', 'notifyWhenNewVersion': 'Notificació d\'actualitzacions', 'analyticsEnabled': 'Puja les estadístiques', - 'analyticsEnabledDesc': 'Compartiu dades d\'ús anònimes per ajudar-nos a millorar', + 'analyticsEnabledDesc': + 'Compartiu dades d\'ús anònimes per ajudar-nos a millorar', 'about': 'Quant a', 'homepage': 'Pàgina d\'inici', 'version': 'Versió', @@ -75,15 +81,18 @@ const caES = { 'logDirectory': 'Carpeta dels registres', 'webhook': 'Webhook', 'webhookEnable': 'Activa el Webhook', - 'webhookDesc': 'Envia notificacions HTTP POST quan les tasques finalitzin o fallin', + 'webhookDesc': + 'Envia notificacions HTTP POST quan les tasques finalitzin o fallin', 'webhookUrlHint': 'Introduïu la URL del webhook', 'webhookTest': 'Prova', 'webhookTestSuccess': 'La prova del webhook ha estat correcta', 'webhookTestFail': 'La prova del webhook ha fallat', 'script': 'Seqüència d\'ordres', 'scriptEnable': 'Activa la seqüència d\'ordres', - 'scriptDesc': 'Executa seqüències d\'ordres personalitzades quan les baixades finalitzin correctament', - 'scriptPathHint': 'Introduïu la ruta del fitxer de seqüències d\'ordres (p.ex., /ruta/al/fitxer.sh)', + 'scriptDesc': + 'Executa seqüències d\'ordres personalitzades quan les baixades finalitzin correctament', + 'scriptPathHint': + 'Introduïu la ruta del fitxer de seqüències d\'ordres (p.ex., /ruta/al/fitxer.sh)', 'urlInvalid': 'Introduïu una URL HTTP o HTTPS vàlida', 'required': 'Aquest camp és obligatori', 'show': 'Mostra', @@ -147,7 +156,8 @@ const caES = { 'browserExtension': 'Extensió del navegador', 'launchAtStartup': 'Executa a l\'inici', 'runAsMenubarApp': 'Executa com a aplicació de barra de menú', - 'runAsMenubarAppDesc': 'Amaga la icona de l\'acoblador i executa només a la barra de menú', + 'runAsMenubarAppDesc': + 'Amaga la icona de l\'acoblador i executa només a la barra de menú', 'seedConfig': 'Configuració de les llavors', 'seedKeep': 'Continua sembrant fins que s\'aturi manualment', 'seedRatio': 'Proporció de les llavors', @@ -161,7 +171,8 @@ const caES = { 'archives': 'Comprimits', 'autoExtract': 'Extreu els comprimits automàticament', 'archivePassword': 'Contrasenya del comprimit', - 'archivePasswordHint': 'Deixeu-lo en blanc si no està protegit amb contrasenya', + 'archivePasswordHint': + 'Deixeu-lo en blanc si no està protegit amb contrasenya', 'deleteAfterExtract': 'Suprimeix el comprimit després de l\'extracció', 'extracting': 'S\'està extraient', 'extractDone': 'Extracció completada', @@ -179,7 +190,8 @@ const caES = { 'username_required': 'Introduïu el vostre nom d\'usuari', 'password_required': 'Introduïu la vostra contrasenya', 'login_success': 'Inici de sessió correcte', - 'login_failed': 'No s\'ha pogut iniciar la sessió, comproveu el vostre nom d\'usuari i contrasenya', + 'login_failed': + 'No s\'ha pogut iniciar la sessió, comproveu el vostre nom d\'usuari i contrasenya', 'login_failed_network': 'No s\'ha pogut iniciar la sessió, comproveu la vostra connexió de xarxa', 'insertPlaceholder': 'Inseriu un marcador de posició', @@ -199,7 +211,8 @@ const caES = { 'selectCategory': 'Seleccioneu una categoria', 'githubMirror': 'Rèplica del GitHub', 'githubMirrorEnable': 'Activa la rèplica del GitHub', - 'githubMirrorDesc': 'Utilitzeu rèpliques per accelerar les baixades de contingut del GitHub', + 'githubMirrorDesc': + 'Utilitzeu rèpliques per accelerar les baixades de contingut del GitHub', 'githubMirrorType': 'Tipus de rèplica', 'githubMirrorUrl': 'URL de la rèplica', 'githubMirrorUrlHint': 'Introduïu la URL de la rèplica', @@ -214,6 +227,17 @@ const caES = { 'La tasca "@name" està esperant l\'actualització de la URL. Voleu actualitzar-la amb la URL nova?', 'pendingUpdateYes': 'Actualitza la tasca', 'pendingUpdateNo': 'Crea una tasca nova', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notificacions d\'escriptori', 'notificationTaskDone': 'Tasca completada', 'notificationTaskError': 'Error de tasca', diff --git a/ui/flutter/lib/i18n/langs/de_de.dart b/ui/flutter/lib/i18n/langs/de_de.dart index 46ecaa3ad..b260fc6fb 100644 --- a/ui/flutter/lib/i18n/langs/de_de.dart +++ b/ui/flutter/lib/i18n/langs/de_de.dart @@ -164,6 +164,17 @@ const deDE = { 'categoryPath': 'Kategoriepfad', 'builtInCategory': 'Integrierte Kategorie kann nicht gelöscht werden', 'selectCategory': 'Kategorie auswählen', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Desktop-Benachrichtigungen', 'notificationTaskDone': 'Aufgabe abgeschlossen', 'notificationTaskError': 'Aufgabenfehler', diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index dd99bd5de..965805f68 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -214,6 +214,17 @@ const enUS = { 'Task "@name" is waiting for URL update. Do you want to update it with the new URL?', 'pendingUpdateYes': 'Update Task', 'pendingUpdateNo': 'Create New Task', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Desktop Notifications', 'notificationTaskDone': 'Task completed', 'notificationTaskError': 'Task error', diff --git a/ui/flutter/lib/i18n/langs/es_es.dart b/ui/flutter/lib/i18n/langs/es_es.dart index c4e9a5954..212447803 100644 --- a/ui/flutter/lib/i18n/langs/es_es.dart +++ b/ui/flutter/lib/i18n/langs/es_es.dart @@ -142,6 +142,17 @@ const esES = { 'categoryPath': 'Ruta de categoría', 'builtInCategory': 'La categoría incorporada no se puede eliminar', 'selectCategory': 'Seleccionar categoría', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notificaciones de escritorio', 'notificationTaskDone': 'Tarea completada', 'notificationTaskError': 'Error de tarea', diff --git a/ui/flutter/lib/i18n/langs/fa_ir.dart b/ui/flutter/lib/i18n/langs/fa_ir.dart index 067719f95..19f047397 100644 --- a/ui/flutter/lib/i18n/langs/fa_ir.dart +++ b/ui/flutter/lib/i18n/langs/fa_ir.dart @@ -93,6 +93,17 @@ const faIR = { 'launchAtStartup': 'راه‌اندازی در استارت‌آپ', 'runAsMenubarApp': 'اجرا به عنوان برنامه نوار منو', 'runAsMenubarAppDesc': 'پنهان کردن آیکون Dock و اجرا فقط در نوار منو', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'اعلان‌های دسکتاپ', 'notificationTaskDone': 'وظیفه تکمیل شد', 'notificationTaskError': 'خطای وظیفه', diff --git a/ui/flutter/lib/i18n/langs/fr_fr.dart b/ui/flutter/lib/i18n/langs/fr_fr.dart index 058660e3a..19c460b5a 100644 --- a/ui/flutter/lib/i18n/langs/fr_fr.dart +++ b/ui/flutter/lib/i18n/langs/fr_fr.dart @@ -135,6 +135,17 @@ const frFR = { 'categoryPath': 'Chemin de la catégorie', 'builtInCategory': 'La catégorie intégrée ne peut pas être supprimée', 'selectCategory': 'Sélectionner une catégorie', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notifications de bureau', 'notificationTaskDone': 'Tâche terminée', 'notificationTaskError': 'Erreur de tâche', diff --git a/ui/flutter/lib/i18n/langs/hu_hu.dart b/ui/flutter/lib/i18n/langs/hu_hu.dart index d0367ce56..5b62802e7 100644 --- a/ui/flutter/lib/i18n/langs/hu_hu.dart +++ b/ui/flutter/lib/i18n/langs/hu_hu.dart @@ -156,6 +156,17 @@ const huHU = { 'categoryPath': 'Kategória útvonala', 'builtInCategory': 'A beépített kategória nem törölhető', 'selectCategory': 'Kategória kiválasztása', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Asztali értesítések', 'notificationTaskDone': 'Feladat befejezve', 'notificationTaskError': 'Feladat hiba', diff --git a/ui/flutter/lib/i18n/langs/id_id.dart b/ui/flutter/lib/i18n/langs/id_id.dart index 23627a76c..366c8abd2 100644 --- a/ui/flutter/lib/i18n/langs/id_id.dart +++ b/ui/flutter/lib/i18n/langs/id_id.dart @@ -142,6 +142,17 @@ const idID = { 'categoryPath': 'Path Kategori', 'builtInCategory': 'Kategori bawaan tidak dapat dihapus', 'selectCategory': 'Pilih Kategori', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notifikasi Desktop', 'notificationTaskDone': 'Tugas selesai', 'notificationTaskError': 'Tugas error', diff --git a/ui/flutter/lib/i18n/langs/it_it.dart b/ui/flutter/lib/i18n/langs/it_it.dart index 4955cf6a8..b912f43c5 100644 --- a/ui/flutter/lib/i18n/langs/it_it.dart +++ b/ui/flutter/lib/i18n/langs/it_it.dart @@ -130,6 +130,17 @@ const itIT = { 'categoryPath': 'Percorso categoria', 'builtInCategory': 'La categoria predefinita non può essere eliminata', 'selectCategory': 'Seleziona categoria', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notifiche Desktop', 'notificationTaskDone': 'Attività completata', 'notificationTaskError': 'Errore attività', diff --git a/ui/flutter/lib/i18n/langs/ja_jp.dart b/ui/flutter/lib/i18n/langs/ja_jp.dart index ab9e8a421..d1a304be1 100644 --- a/ui/flutter/lib/i18n/langs/ja_jp.dart +++ b/ui/flutter/lib/i18n/langs/ja_jp.dart @@ -95,6 +95,17 @@ const jaJP = { 'launchAtStartup': '起動時に起動', 'runAsMenubarApp': 'メニューバーアプリとして実行', 'runAsMenubarAppDesc': 'Dockアイコンを非表示にして、メニューバーのみで実行', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'デスクトップ通知', 'notificationTaskDone': 'タスク完了', 'notificationTaskError': 'タスクエラー', diff --git a/ui/flutter/lib/i18n/langs/pl_pl.dart b/ui/flutter/lib/i18n/langs/pl_pl.dart index 6721099bb..dfe230e18 100644 --- a/ui/flutter/lib/i18n/langs/pl_pl.dart +++ b/ui/flutter/lib/i18n/langs/pl_pl.dart @@ -127,6 +127,17 @@ const plPL = { 'categoryPath': 'Ścieżka kategorii', 'builtInCategory': 'Wbudowanej kategorii nie można usunąć', 'selectCategory': 'Wybierz kategorię', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Powiadomienia na pulpicie', 'notificationTaskDone': 'Zadanie zakończone', 'notificationTaskError': 'Błąd zadania', diff --git a/ui/flutter/lib/i18n/langs/pt_br.dart b/ui/flutter/lib/i18n/langs/pt_br.dart index 760b87538..df1f7f331 100644 --- a/ui/flutter/lib/i18n/langs/pt_br.dart +++ b/ui/flutter/lib/i18n/langs/pt_br.dart @@ -153,6 +153,17 @@ const ptBR = { 'categoryPath': 'Caminho da Categoria', 'builtInCategory': 'Categoria integrada não pode ser excluída', 'selectCategory': 'Selecionar Categoria', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Notificações na área de trabalho', 'notificationTaskDone': 'Tarefa concluída', 'notificationTaskError': 'Erro na tarefa', diff --git a/ui/flutter/lib/i18n/langs/ru_ru.dart b/ui/flutter/lib/i18n/langs/ru_ru.dart index 8103718a2..fdfed25d3 100644 --- a/ui/flutter/lib/i18n/langs/ru_ru.dart +++ b/ui/flutter/lib/i18n/langs/ru_ru.dart @@ -152,6 +152,17 @@ const ruRU = { 'categoryPath': 'Путь категории', 'builtInCategory': 'Встроенную категорию нельзя удалить', 'selectCategory': 'Выбрать категорию', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Уведомления', 'notificationTaskDone': 'Задача завершена', 'notificationTaskError': 'Ошибка задачи', diff --git a/ui/flutter/lib/i18n/langs/ta_ta.dart b/ui/flutter/lib/i18n/langs/ta_ta.dart index 78a0cfbeb..a287c9166 100644 --- a/ui/flutter/lib/i18n/langs/ta_ta.dart +++ b/ui/flutter/lib/i18n/langs/ta_ta.dart @@ -129,6 +129,17 @@ const taTA = { 'categoryPath': 'வகை பாதை', 'builtInCategory': 'உள்ளமைக்கப்பட்ட வகையை நீக்க முடியாது', 'selectCategory': 'வகையைத் தேர்ந்தெடுக்கவும்', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'கணினி அறிவிப்புகள்', 'notificationTaskDone': 'பணி முடிந்தது', 'notificationTaskError': 'பணி பிழை', diff --git a/ui/flutter/lib/i18n/langs/tr_tr.dart b/ui/flutter/lib/i18n/langs/tr_tr.dart index db8fbb3aa..ac23b3417 100644 --- a/ui/flutter/lib/i18n/langs/tr_tr.dart +++ b/ui/flutter/lib/i18n/langs/tr_tr.dart @@ -22,8 +22,10 @@ const trTR = { 'followSettings': 'Ayarları Takip Et', 'downloadLink': 'İndirme Bağlantısı', 'downloadLinkValid': 'Lütfen indirme bağlantısını girin', - 'downloadLinkHit': 'Lütfen her satıra bir tane indirme bağlantısını girin@append', - 'downloadLinkHitDesktop': ', veya torrent dosyasını olarak buraya sürükleyin', + 'downloadLinkHit': + 'Lütfen her satıra bir tane indirme bağlantısını girin@append', + 'downloadLinkHitDesktop': + ', veya torrent dosyasını olarak buraya sürükleyin', 'download': 'İndir', 'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.', 'noStoragePermission': 'Depolama izni gerekli', @@ -39,7 +41,8 @@ const trTR = { 'maxRunning': 'Maksimum Çalışan İşlem', 'defaultDirectDownload': 'Varsayılan olarak direkt indirmeyi işaretle', 'autoStartTasks': 'Başlangıçta tamamlanmamış işlemleri otomatik başlat', - 'autoTorrentEnable': '.torrent dosyalarından otomatik BitTorrent işlemleri oluştur', + 'autoTorrentEnable': + '.torrent dosyalarından otomatik BitTorrent işlemleri oluştur', 'autoTorrentDeleteAfterDownload': 'BitTorrent işlemi oluşturulduktan sonra .torrent dosyasını sil', 'autoDeleteMissingFileTasks': 'Eksik dosya işlemlerini otomatik sil', @@ -51,7 +54,8 @@ const trTR = { 'updateDaily': 'Günlük güncelle', 'lastUpdate': 'Son güncelleme: @time', 'addTracker': 'Tracker Ekle', - 'addTrackerHit': 'Lütfen her satıra bir tane olacak şekilde tracker sunucu adresini girin', + 'addTrackerHit': + 'Lütfen her satıra bir tane olacak şekilde tracker sunucu adresini girin', 'ui': 'Arayüz', 'theme': 'Tema', 'themeSystem': 'Sistem', @@ -60,7 +64,8 @@ const trTR = { 'locale': 'Dil', 'notifyWhenNewVersion': 'Güncellemeleri bildir', 'analyticsEnabled': 'İstatistikleri Yükle', - 'analyticsEnabledDesc': 'Uygulamayı geliştirmemize yardımcı olmak için anonim kullanım verilerini paylaşın', + 'analyticsEnabledDesc': + 'Uygulamayı geliştirmemize yardımcı olmak için anonim kullanım verilerini paylaşın', 'about': 'Hakkında', 'homepage': 'Ana Sayfa', 'version': 'Sürüm', @@ -75,14 +80,16 @@ const trTR = { 'logDirectory': 'Günlük Dizini', 'webhook': 'Webhook', 'webhookEnable': 'Webhook\'u Etkinleştir', - 'webhookDesc': 'İşlemler tamamlandığında veya başarısız olduğunda HTTP POST bildirimleri gönder', + 'webhookDesc': + 'İşlemler tamamlandığında veya başarısız olduğunda HTTP POST bildirimleri gönder', 'webhookUrlHint': 'Webhook URL\'sini girin', 'webhookTest': 'Test', 'webhookTestSuccess': 'Webhook testi başarılı', 'webhookTestFail': 'Webhook testi başarısız', 'script': 'Betik', 'scriptEnable': 'Betiği Etkinleştir', - 'scriptDesc': 'İndirmeler başarıyla tamamlandığında özel betikleri çalıştır', + 'scriptDesc': + 'İndirmeler başarıyla tamamlandığında özel betikleri çalıştır', 'scriptPathHint': 'Betik dosya yolunu girin (örn. /path/to/script.sh)', 'urlInvalid': 'Lütfen geçerli bir HTTP veya HTTPS URL\'si girin', 'required': 'Bu alan zorunludur', @@ -147,7 +154,8 @@ const trTR = { 'browserExtension': 'Tarayıcı Eklentisi', 'launchAtStartup': 'Başlangıçta çalıştır', 'runAsMenubarApp': 'Menü çubuğu uygulaması olarak çalıştır', - 'runAsMenubarAppDesc': 'Dock simgesini gizle ve sadece menü çubuğunda çalıştır', + 'runAsMenubarAppDesc': + 'Dock simgesini gizle ve sadece menü çubuğunda çalıştır', 'seedConfig': 'Seed Yapılandırması', 'seedKeep': 'Manuel olarak durdurulana kadar seed\'e devam et', 'seedRatio': 'Seed oranı', @@ -179,7 +187,8 @@ const trTR = { 'username_required': 'Lütfen kullanıcı adınızı girin', 'password_required': 'Lütfen şifrenizi girin', 'login_success': 'Giriş başarılı', - 'login_failed': 'Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin', + 'login_failed': + 'Giriş başarısız, lütfen kullanıcı adınızı ve şifrenizi kontrol edin', 'login_failed_network': 'Giriş başarısız, lütfen ağ bağlantınızı kontrol edin', 'insertPlaceholder': 'Yer Tutucu Ekle', @@ -199,7 +208,8 @@ const trTR = { 'selectCategory': 'Kategori Seç', 'githubMirror': 'GitHub Yansısı', 'githubMirrorEnable': 'GitHub Yansısını Etkinleştir', - 'githubMirrorDesc': 'GitHub içerik indirmelerini hızlandırmak için yansıları kullanın', + 'githubMirrorDesc': + 'GitHub içerik indirmelerini hızlandırmak için yansıları kullanın', 'githubMirrorType': 'Yansı Türü', 'githubMirrorUrl': 'Yansı URL\'si', 'githubMirrorUrlHint': 'Yansı URL\'sini girin', @@ -210,10 +220,21 @@ const trTR = { 'updateUrlCancelListen': 'İzlemeyi İptal Et', 'updateUrlDialogHint': 'Yeni indirme URL\'sini girin', 'pendingUpdateFound': 'Bekleyen Güncelleme İşlemi Bulundu', - 'pendingUpdateConfirm': + 'pendingUpdateConfirm': '"@name" işlemi URL güncellemesi bekliyor. Yeni URL ile güncellemek istiyor musunuz?', 'pendingUpdateYes': 'İşlemi Güncelle', 'pendingUpdateNo': 'Yeni İşlem Oluştur', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Masaüstü Bildirimleri', 'notificationTaskDone': 'Görev tamamlandı', 'notificationTaskError': 'Görev hatası', diff --git a/ui/flutter/lib/i18n/langs/uk_ua.dart b/ui/flutter/lib/i18n/langs/uk_ua.dart index 6971776c1..bfb068f1d 100644 --- a/ui/flutter/lib/i18n/langs/uk_ua.dart +++ b/ui/flutter/lib/i18n/langs/uk_ua.dart @@ -152,6 +152,17 @@ const ukUA = { 'categoryPath': 'Шлях категорії', 'builtInCategory': 'Вбудовану категорію неможливо видалити', 'selectCategory': 'Вибрати категорію', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Сповіщення', 'notificationTaskDone': 'Завдання завершено', 'notificationTaskError': 'Помилка завдання', diff --git a/ui/flutter/lib/i18n/langs/vi_vn.dart b/ui/flutter/lib/i18n/langs/vi_vn.dart index b27593132..941f1caa1 100644 --- a/ui/flutter/lib/i18n/langs/vi_vn.dart +++ b/ui/flutter/lib/i18n/langs/vi_vn.dart @@ -122,6 +122,17 @@ const viVN = { 'launchAtStartup': 'Khởi động cùng hệ thống', 'runAsMenubarApp': 'Chạy như ứng dụng thanh menu', 'runAsMenubarAppDesc': 'Ẩn biểu tượng Dock và chỉ chạy trong thanh menu', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP Listen Port', + 'ed2kUdpPort': 'UDP Listen Port', + 'ed2kServerList': 'Server Addresses', + 'ed2kServerMet': 'Server.met Sources', + 'ed2kNodesDat': 'nodes.dat Sources', + 'ed2kAutoPort': 'Auto assign', + 'ed2kOnePerLine': 'One entry per line', + 'ed2kServersHint': 'host:port, one per line', + 'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line', + 'ed2kNodesDatHint': 'nodes.dat path or URL, one per line', 'desktopNotification': 'Thông báo trên màn hình', 'notificationTaskDone': 'Nhiệm vụ hoàn thành', 'notificationTaskError': 'Lỗi nhiệm vụ', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index e26b0c949..53cd1a368 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -209,6 +209,17 @@ const zhCN = { 'pendingUpdateConfirm': '任务 "@name" 正在等待更新地址,是否使用新地址更新该任务?', 'pendingUpdateYes': '更新任务', 'pendingUpdateNo': '创建新任务', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP 监听端口', + 'ed2kUdpPort': 'UDP 监听端口', + 'ed2kServerList': '服务器地址', + 'ed2kServerMet': 'Server.met 源', + 'ed2kNodesDat': 'nodes.dat 源', + 'ed2kAutoPort': '自动分配', + 'ed2kOnePerLine': '每行填写一条', + 'ed2kServersHint': 'host:port,每行一条', + 'ed2kServerMetHint': '请输入 Server.met 地址或 ed2k 服务器列表链接,每行一条', + 'ed2kNodesDatHint': '请输入 nodes.dat 本地路径或 URL,每行一条', 'desktopNotification': '桌面通知', 'notificationTaskDone': '任务完成', 'notificationTaskError': '任务失败', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 004e2f713..1b46bb1a3 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -147,6 +147,17 @@ const zhTW = { 'categoryPath': '分類路徑', 'builtInCategory': '內建分類無法刪除', 'selectCategory': '選擇分類', + 'ed2k': 'ED2K', + 'ed2kTcpPort': 'TCP 監聽連接埠', + 'ed2kUdpPort': 'UDP 監聽連接埠', + 'ed2kServerList': '伺服器位址', + 'ed2kServerMet': 'Server.met 來源', + 'ed2kNodesDat': 'nodes.dat 來源', + 'ed2kAutoPort': '自動分配', + 'ed2kOnePerLine': '每行填寫一條', + 'ed2kServersHint': 'host:port,每行一條', + 'ed2kServerMetHint': '請輸入 Server.met 位址或 ed2k 伺服器清單連結,每行一條', + 'ed2kNodesDatHint': '請輸入 nodes.dat 本機路徑或 URL,每行一條', 'desktopNotification': '桌面通知', 'notificationTaskDone': '任務完成', 'notificationTaskError': '任務失敗', From 9e7f0bfa8a785063c25a93f5ed39988fbe6ff257 Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 17 Mar 2026 17:58:12 +0800 Subject: [PATCH 2/3] update test case --- internal/protocol/ed2k/fetcher_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/protocol/ed2k/fetcher_test.go b/internal/protocol/ed2k/fetcher_test.go index 16dcd79ee..a613a21df 100644 --- a/internal/protocol/ed2k/fetcher_test.go +++ b/internal/protocol/ed2k/fetcher_test.go @@ -7,7 +7,7 @@ import ( "github.com/GopeedLab/gopeed/pkg/base" ) -const testLink = "ed2k://|file|Taylor,%20Elizabeth%20-%20Prohibido%20morir%20aqui%20[66672]%20(r1.0).epub|434885|23A8CEFF57A7A32D562D649ED7893796|/" +const testLink = "ed2k://|file|cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso|4630972416|8867C5E54405FF9452225B66EFEE690A|/" func TestFetcher_Resolve(t *testing.T) { f := (&FetcherManager{}).Build() @@ -22,20 +22,20 @@ func TestFetcher_Resolve(t *testing.T) { if meta.Res == nil { t.Fatal("Resolve() resource is nil") } - if got, want := meta.Res.Hash, "23A8CEFF57A7A32D562D649ED7893796"; got != want { + if got, want := meta.Res.Hash, "8867C5E54405FF9452225B66EFEE690A"; got != want { t.Fatalf("Resolve() hash = %s, want %s", got, want) } - if got, want := meta.Res.Size, int64(434885); got != want { + if got, want := meta.Res.Size, int64(4630972416); got != want { t.Fatalf("Resolve() size = %d, want %d", got, want) } - if got, want := meta.Res.Files[0].Name, "Taylor, Elizabeth - Prohibido morir aqui [66672] (r1.0).epub"; got != want { + if got, want := meta.Res.Files[0].Name, "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso"; got != want { t.Fatalf("Resolve() name = %q, want %q", got, want) } } func TestFetcherManager_ParseName(t *testing.T) { got := (&FetcherManager{}).ParseName(testLink) - want := "Taylor, Elizabeth - Prohibido morir aqui [66672] (r1.0).epub" + want := "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso" if got != want { t.Fatalf("ParseName() = %q, want %q", got, want) } From 18b3a67e388579381361fc6b69d68c60df9c25bc Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 17 Mar 2026 18:07:16 +0800 Subject: [PATCH 3/3] fix gomobile --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ed48d0fc1..7b27ae99f 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/klauspost/pgzip v1.2.6 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect - github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 // indirect + github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect diff --git a/go.sum b/go.sum index d0c935b16..c6a406355 100644 --- a/go.sum +++ b/go.sum @@ -388,6 +388,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 h1:tEOucyKJzFsCz5Gr41jFHj8i2g1zemTyS4uyErJgFHc= github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg= +github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 h1:88kRsNwkDKPA4NzRUWdOoIL7SyMNDwtmAxzShKBvKTI= +github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=