mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 09:30:27 +00:00
feat: ed2k protocol support (#1316)
This commit is contained in:
35 files changed
+1129
-35
No files matched your search
@@ -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-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
|
||||
|
||||
@@ -386,6 +386,10 @@ 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/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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package ed2k
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
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()
|
||||
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, "8867C5E54405FF9452225B66EFEE690A"; got != want {
|
||||
t.Fatalf("Resolve() hash = %s, want %s", 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, "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 := "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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$Ed2kConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$Ed2kConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ExtraConfig {
|
||||
String themeMode;
|
||||
|
||||
@@ -42,12 +42,15 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
|
||||
ProtocolConfig()
|
||||
..http = HttpConfig.fromJson(json['http'] as Map<String, dynamic>)
|
||||
..bt = BtConfig.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
..bt = BtConfig.fromJson(json['bt'] as Map<String, dynamic>)
|
||||
..ed2k = Ed2kConfig.fromJson(
|
||||
json['ed2k'] as Map<String, dynamic>? ?? <String, dynamic>{});
|
||||
|
||||
Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'http': instance.http.toJson(),
|
||||
'bt': instance.bt.toJson(),
|
||||
'ed2k': instance.ed2k.toJson(),
|
||||
};
|
||||
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||
@@ -82,6 +85,23 @@ Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||
'seedTime': instance.seedTime,
|
||||
};
|
||||
|
||||
Ed2kConfig _$Ed2kConfigFromJson(Map<String, dynamic> 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<String, dynamic> _$Ed2kConfigToJson(Ed2kConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'listenPort': instance.listenPort,
|
||||
'udpPort': instance.udpPort,
|
||||
'serverAddr': instance.serverAddr,
|
||||
'serverMet': instance.serverMet,
|
||||
'nodesDat': instance.nodesDat,
|
||||
};
|
||||
|
||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
themeMode: json['themeMode'] as String? ?? '',
|
||||
locale: json['locale'] as String? ?? '',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -51,6 +51,7 @@ const _$StatusEnumMap = {
|
||||
const _$ProtocolEnumMap = {
|
||||
Protocol.http: 'http',
|
||||
Protocol.bt: 'bt',
|
||||
Protocol.ed2k: 'ed2k',
|
||||
};
|
||||
|
||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||
|
||||
@@ -59,7 +59,7 @@ class CreateView extends GetView<CreateController> {
|
||||
final _btTrackerController = TextEditingController();
|
||||
final _archivePasswordController = TextEditingController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
final _availableSchemes = ["http:", "https:", "magnet:", "ed2k:"];
|
||||
|
||||
final _skipVerifyCertController = false.obs;
|
||||
final _autoTorrentController = Rxn<bool>();
|
||||
@@ -116,6 +116,7 @@ class CreateView extends GetView<CreateController> {
|
||||
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<CreateController> {
|
||||
}
|
||||
},
|
||||
Protocol.bt: null,
|
||||
Protocol.ed2k: null,
|
||||
};
|
||||
if (routerParams.opts?.extra != null) {
|
||||
optionsHandlers[protocol]?.call();
|
||||
@@ -804,6 +806,9 @@ class CreateView extends GetView<CreateController> {
|
||||
uppercaseUrl.endsWith(".TORRENT")) {
|
||||
protocol = Protocol.bt;
|
||||
}
|
||||
if (uppercaseUrl.startsWith("ED2K:")) {
|
||||
protocol = Protocol.ed2k;
|
||||
}
|
||||
return protocol;
|
||||
}
|
||||
|
||||
@@ -1007,6 +1012,9 @@ class CreateView extends GetView<CreateController> {
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
break;
|
||||
case Protocol.ed2k:
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
return reqExtra;
|
||||
}
|
||||
|
||||
@@ -501,14 +501,16 @@ class SettingView extends GetView<SettingController> {
|
||||
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<SettingController> {
|
||||
);
|
||||
});
|
||||
|
||||
// ed2k config items start
|
||||
final ed2kConfig = downloaderCfg.value.protocolConfig.ed2k;
|
||||
List<String> 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<SettingController> {
|
||||
buildBtDefaultClientConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ed2k'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildEd2kListenPort(),
|
||||
buildEd2kUdpPort(),
|
||||
buildEd2kServerAddr(),
|
||||
buildEd2kServerMet(),
|
||||
buildEd2kNodesDat(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'خطای وظیفه',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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à',
|
||||
|
||||
@@ -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': 'タスクエラー',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Ошибка задачи',
|
||||
|
||||
@@ -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': 'பணி பிழை',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -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': 'Помилка завдання',
|
||||
|
||||
@@ -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ụ',
|
||||
|
||||
@@ -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': '任务失败',
|
||||
|
||||
@@ -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': '任務失敗',
|
||||
|
||||
Reference in new issue
Block a user