feat: add config for auto-torrent (#1219)

This commit is contained in:
Copilot authored and GitHub committed 2026-01-22 22:18:34 +08:00
1 parent a3c1f84c2c
commit 7b840b4261
31 files changed
+742 -131

No files matched your search

+24 -11
View File
@@ -178,17 +178,15 @@ type CreateTaskBatchItem struct {
type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
Webhook *WebhookConfig `json:"webhook"` // Webhook is the webhook configuration
Archive *ArchiveConfig `json:"archive"` // Archive is the archive extraction configuration
// New fields added for auto-management
AutoDeleteTorrents bool `json:"autoDeleteTorrents"` // AutoDeleteTorrents enables automatic deletion of .torrent files after download
AutoCleanMissingFiles bool `json:"autoCleanMissingFiles"` // AutoCleanMissingFiles enables automatic cleanup of tasks with missing files
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
Webhook *WebhookConfig `json:"webhook"` // Webhook is the webhook configuration
AutoTorrent *AutoTorrentConfig `json:"autoTorrent"` // AutoTorrent is the auto torrent task creation configuration
Archive *ArchiveConfig `json:"archive"` // Archive is the archive extraction configuration
AutoDeleteMissingFileTasks bool `json:"autoDeleteMissingFileTasks"` // AutoDeleteMissingFileTasks enables automatic deletion of tasks with missing files
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
@@ -204,6 +202,12 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.Webhook == nil {
cfg.Webhook = &WebhookConfig{}
}
if cfg.AutoTorrent == nil {
cfg.AutoTorrent = &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
}
}
if cfg.Archive == nil {
cfg.Archive = &ArchiveConfig{
AutoExtract: false,
@@ -235,6 +239,9 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
if cfg.Webhook == nil {
cfg.Webhook = beforeCfg.Webhook
}
if cfg.AutoTorrent == nil {
cfg.AutoTorrent = beforeCfg.AutoTorrent
}
if cfg.Archive == nil {
cfg.Archive = beforeCfg.Archive
}
@@ -247,6 +254,12 @@ type WebhookConfig struct {
URLs []string `json:"urls"` // URLs is the list of webhook URLs
}
// AutoTorrentConfig is the auto torrent task creation configuration
type AutoTorrentConfig struct {
Enable bool `json:"enable"` // Enable enables automatic BT task creation when downloading .torrent files
DeleteAfterDownload bool `json:"deleteAfterDownload"` // DeleteAfterDownload deletes the .torrent file after BT task creation
}
// ArchiveConfig is the archive extraction configuration
type ArchiveConfig struct {
AutoExtract bool `json:"autoExtract"` // AutoExtract enables automatic extraction of archives after download
+84
View File
@@ -19,6 +19,10 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
@@ -35,6 +39,10 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
@@ -55,6 +63,10 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
@@ -75,6 +87,33 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
Enable: true,
},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
{
"Init AutoTorrent",
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
@@ -94,6 +133,10 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: true,
DeleteAfterExtract: false,
@@ -111,6 +154,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
AutoTorrent: tt.fields.AutoTorrent,
Archive: tt.fields.Archive,
}
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
@@ -286,6 +330,45 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
},
},
},
{
"Merge AutoTorrent No Override",
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
},
},
args{
beforeCfg: &DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
},
},
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
},
},
},
{
"Merge AutoTorrent Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
},
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
},
{
"Merge Archive No Override",
&DownloaderStoreConfig{
@@ -336,6 +419,7 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
AutoTorrent: tt.fields.AutoTorrent,
Archive: tt.fields.Archive,
}
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
+32 -9
View File
@@ -283,7 +283,7 @@ func (d *Downloader) cleanupNonExistingTasks() {
}
// If the feature is disabled, do nothing
if !cfg.AutoCleanMissingFiles {
if !cfg.AutoDeleteMissingFileTasks {
return
}
@@ -957,10 +957,25 @@ func (d *Downloader) watch(task *Task) {
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
downloadFilePath := task.Meta.SingleFilepath()
if e.AutoTorrent && strings.HasSuffix(downloadFilePath, ".torrent") {
cfg, err := d.GetConfig()
shouldDelete := err == nil && cfg.AutoDeleteTorrents
cfg, _ := d.GetConfig()
// Determine if auto-torrent is enabled (use global config if not explicitly set)
autoTorrentEnabled := false
if e.AutoTorrent != nil {
autoTorrentEnabled = *e.AutoTorrent
} else if cfg != nil && cfg.AutoTorrent != nil {
autoTorrentEnabled = cfg.AutoTorrent.Enable
}
if autoTorrentEnabled && strings.HasSuffix(downloadFilePath, ".torrent") {
// Determine if should delete torrent file after creating BT task
shouldDelete := false
if e.DeleteTorrentAfterDownload != nil {
shouldDelete = *e.DeleteTorrentAfterDownload
} else if cfg != nil && cfg.AutoTorrent != nil {
shouldDelete = cfg.AutoTorrent.DeleteAfterDownload
}
go func() {
_, err2 := d.CreateDirect(
@@ -973,18 +988,26 @@ func (d *Downloader) watch(task *Task) {
})
if err2 != nil {
d.Logger.Error().Err(err2).Msgf("auto create torrent task failed, task id: %s", task.ID)
} else {
return
}
if shouldDelete {
d.Delete(&TaskFilter{IDs: []string{task.ID}}, true)
}
if shouldDelete {
d.Delete(&TaskFilter{IDs: []string{task.ID}}, true)
}
}()
}
// Determine if auto-extract is enabled (use global config if not explicitly set)
autoExtractEnabled := false
if e.AutoExtract != nil {
autoExtractEnabled = *e.AutoExtract
} else if cfg != nil && cfg.Archive != nil {
autoExtractEnabled = cfg.Archive.AutoExtract
}
// Auto-extract archive files using the extraction queue
// This ensures only one extraction runs at a time to prevent resource exhaustion
if e.AutoExtract && isArchiveFile(downloadFilePath) {
if autoExtractEnabled && isArchiveFile(downloadFilePath) {
d.enqueueExtraction(task, downloadFilePath, e)
}
}
+319 -3
View File
@@ -18,6 +18,7 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
)
var testDownloadOpt = &base.Options{
@@ -1037,7 +1038,7 @@ func TestDownloader_AutoExtractWithProgress(t *testing.T) {
Name: "archive.zip",
Extra: http.OptsExtra{
Connections: 1,
AutoExtract: true,
AutoExtract: util.BoolPtr(true),
},
})
if err != nil {
@@ -1164,7 +1165,7 @@ func TestDownloader_AutoExtractWithDeleteAfterExtract(t *testing.T) {
Name: "archive.zip",
Extra: http.OptsExtra{
Connections: 1,
AutoExtract: true,
AutoExtract: util.BoolPtr(true),
DeleteAfterExtract: true,
},
})
@@ -1258,7 +1259,7 @@ func TestDownloader_AutoExtractError(t *testing.T) {
Name: "corrupt.zip",
Extra: http.OptsExtra{
Connections: 1,
AutoExtract: true,
AutoExtract: util.BoolPtr(true),
},
})
if err != nil {
@@ -2332,3 +2333,318 @@ func TestDownloader_CheckMultiPartArchiveReady_EmptyBaseName(t *testing.T) {
t.Error("checkMultiPartArchiveReady() should return true for non-multi-part files")
}
}
// startTestTorrentServer starts a simple HTTP server that serves a torrent file
func startTestTorrentServer(torrentPath string) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
server := &gohttp.Server{
Handler: gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
data, err := os.ReadFile(torrentPath)
if err != nil {
w.WriteHeader(gohttp.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/x-bittorrent")
w.Header().Set("Content-Disposition", "attachment; filename=ubuntu.torrent")
w.Write(data)
}),
}
go server.Serve(listener)
return listener
}
// TestDownloader_AutoTorrent tests the auto-torrent functionality
// When a .torrent file is downloaded with AutoTorrent enabled, it should automatically create a BT task
func TestDownloader_AutoTorrent(t *testing.T) {
// Path to the test torrent file
torrentPath := "../../internal/protocol/bt/testdata/ubuntu-22.04-live-server-amd64.iso.torrent"
if _, err := os.Stat(torrentPath); os.IsNotExist(err) {
t.Skip("Test torrent file not found, skipping test")
}
// Start a simple HTTP server to serve the torrent file
server := startTestTorrentServer(torrentPath)
defer server.Close()
// Create a temporary directory for the test
tempDir, err := os.MkdirTemp("", "auto_torrent_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create downloader
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer func() {
// Delete all tasks before clearing to avoid panic from BT tasks trying to access deleted resources
downloader.Delete(nil, true)
downloader.Clear()
}()
// Track created tasks
btTaskCreated := make(chan struct{}, 1)
var originalTaskId string
downloader.Listener(func(event *Event) {
if event.Key == EventKeyStart {
// A new task started - if it's not the original, it's the BT task
if event.Task != nil && event.Task.ID != originalTaskId && originalTaskId != "" {
select {
case btTaskCreated <- struct{}{}:
default:
}
}
}
})
// Create request to download the torrent file
req := &base.Request{
URL: "http://" + server.Addr().String() + "/ubuntu.torrent",
}
// Create task with AutoTorrent enabled
downloadDir := tempDir + "/downloads"
originalTaskId, err = downloader.CreateDirect(req, &base.Options{
Path: downloadDir,
Name: "ubuntu.torrent",
Extra: http.OptsExtra{
Connections: 1,
AutoTorrent: util.BoolPtr(true),
},
})
if err != nil {
t.Fatal(err)
}
t.Logf("Original task ID: %s", originalTaskId)
// Wait for BT task to be created (with timeout)
select {
case <-btTaskCreated:
t.Log("BT task created")
case <-time.After(10 * time.Second):
t.Log("Timeout waiting for BT task creation")
}
// Give a small buffer for task creation to complete
time.Sleep(200 * time.Millisecond)
// Verify that a BT task was created
tasks := downloader.GetTasks()
// At minimum, we should have 2 tasks: the original torrent download and the BT task
if len(tasks) < 2 {
t.Errorf("Expected at least 2 tasks (torrent download + BT task), got %d", len(tasks))
} else {
t.Logf("Successfully created %d tasks", len(tasks))
}
}
// TestDownloader_AutoTorrentWithDelete tests the auto-torrent with DeleteTorrentAfterDownload option
func TestDownloader_AutoTorrentWithDelete(t *testing.T) {
// Path to the test torrent file
torrentPath := "../../internal/protocol/bt/testdata/ubuntu-22.04-live-server-amd64.iso.torrent"
if _, err := os.Stat(torrentPath); os.IsNotExist(err) {
t.Skip("Test torrent file not found, skipping test")
}
// Start a simple HTTP server to serve the torrent file
server := startTestTorrentServer(torrentPath)
defer server.Close()
// Create a temporary directory for the test
tempDir, err := os.MkdirTemp("", "auto_torrent_delete_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create downloader
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer func() {
// Delete all tasks before clearing to avoid panic from BT tasks trying to access deleted resources
downloader.Delete(nil, true)
downloader.Clear()
}()
// Track task events
var originalTaskId string
originalTaskDeleted := make(chan struct{}, 1)
btTaskCreated := make(chan struct{}, 1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyStart {
// BT task was created and started
if event.Task != nil && event.Task.ID != originalTaskId && originalTaskId != "" {
select {
case btTaskCreated <- struct{}{}:
default:
}
}
}
if event.Key == EventKeyDelete {
// Check if the deleted task is the original torrent task
if event.Task != nil && event.Task.ID == originalTaskId {
select {
case originalTaskDeleted <- struct{}{}:
default:
}
}
}
})
// Create request to download the torrent file
req := &base.Request{
URL: "http://" + server.Addr().String() + "/ubuntu.torrent",
}
// Create task with AutoTorrent and DeleteTorrentAfterDownload enabled
downloadDir := tempDir + "/downloads"
originalTaskId, err = downloader.CreateDirect(req, &base.Options{
Path: downloadDir,
Name: "ubuntu.torrent",
Extra: http.OptsExtra{
Connections: 1,
AutoTorrent: util.BoolPtr(true),
DeleteTorrentAfterDownload: util.BoolPtr(true),
},
})
if err != nil {
t.Fatal(err)
}
t.Logf("Original task ID: %s", originalTaskId)
// Wait for original task to be deleted (this happens after BT task is created)
select {
case <-originalTaskDeleted:
t.Log("Original torrent task was deleted as expected")
case <-time.After(10 * time.Second):
// Check manually if the task still exists
originalTask := downloader.GetTask(originalTaskId)
if originalTask != nil {
t.Error("Original torrent task should have been deleted but still exists")
} else {
t.Log("Original torrent task was deleted (detected via GetTask)")
}
}
// Give a moment for BT task creation
time.Sleep(200 * time.Millisecond)
// Verify that original task is deleted and BT task exists
originalTask := downloader.GetTask(originalTaskId)
if originalTask != nil {
t.Error("Original torrent task should have been deleted")
}
// Verify remaining tasks (should have at least the BT task)
tasks := downloader.GetTasks()
t.Logf("Remaining tasks: %d", len(tasks))
// At least one task should remain (the BT task)
if len(tasks) == 0 {
t.Error("Expected at least one task (BT task) to remain")
}
// None of the remaining tasks should be the original torrent task
for _, task := range tasks {
if task.ID == originalTaskId {
t.Error("Original torrent task should have been deleted")
}
}
}
// TestDownloader_AutoTorrentDisabled tests that auto-torrent does not create BT task when disabled
func TestDownloader_AutoTorrentDisabled(t *testing.T) {
// Path to the test torrent file
torrentPath := "../../internal/protocol/bt/testdata/ubuntu-22.04-live-server-amd64.iso.torrent"
if _, err := os.Stat(torrentPath); os.IsNotExist(err) {
t.Skip("Test torrent file not found, skipping test")
}
// Start a simple HTTP server to serve the torrent file
server := startTestTorrentServer(torrentPath)
defer server.Close()
// Create a temporary directory for the test
tempDir, err := os.MkdirTemp("", "auto_torrent_disabled_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create downloader
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer func() {
downloader.Delete(nil, true)
downloader.Clear()
}()
// Track task completion
taskDone := make(chan struct{}, 1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
select {
case taskDone <- struct{}{}:
default:
}
}
})
// Create request to download the torrent file
req := &base.Request{
URL: "http://" + server.Addr().String() + "/ubuntu.torrent",
}
// Create task with AutoTorrent explicitly disabled
downloadDir := tempDir + "/downloads"
_, err = downloader.CreateDirect(req, &base.Options{
Path: downloadDir,
Name: "ubuntu.torrent",
Extra: http.OptsExtra{
Connections: 1,
AutoTorrent: util.BoolPtr(false),
},
})
if err != nil {
t.Fatal(err)
}
// Wait for task to complete
select {
case <-taskDone:
// Task completed
case <-time.After(10 * time.Second):
t.Fatal("Timeout waiting for task to complete")
}
// Give a small buffer
time.Sleep(200 * time.Millisecond)
// Verify that only 1 task exists (no BT task was created)
tasks := downloader.GetTasks()
if len(tasks) != 1 {
t.Errorf("Expected exactly 1 task (torrent download only), got %d", len(tasks))
}
// Verify the torrent file was downloaded
torrentFilePath := downloadDir + "/ubuntu.torrent"
if _, err := os.Stat(torrentFilePath); os.IsNotExist(err) {
t.Error("Torrent file should have been downloaded")
}
}
+7 -2
View File
@@ -9,9 +9,14 @@ type ReqExtra struct {
type OptsExtra struct {
Connections int `json:"connections"`
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
AutoTorrent bool `json:"autoTorrent"`
// nil means use global config, true/false means explicit setting
AutoTorrent *bool `json:"autoTorrent"`
// DeleteTorrentAfterDownload when true, deletes the .torrent file after creating BT task
// nil means use global config, true/false means explicit setting
DeleteTorrentAfterDownload *bool `json:"deleteTorrentAfterDownload"`
// AutoExtract when task download complete, and it is an archive file, it will be auto extracted
AutoExtract bool `json:"autoExtract"`
// nil means use global config, true/false means explicit setting
AutoExtract *bool `json:"autoExtract"`
// ArchivePassword is the password for extracting password-protected archives
ArchivePassword string `json:"archivePassword"`
// DeleteAfterExtract when true, deletes the archive file after successful extraction
+10
View File
@@ -26,3 +26,13 @@ func DeepClone[T any](v *T) *T {
json.Unmarshal(b, &t)
return &t
}
// Ptr returns a pointer to the given value.
func Ptr[T any](v T) *T {
return &v
}
// BoolPtr returns a pointer to a bool value.
func BoolPtr(v bool) *bool {
return &v
}
@@ -10,11 +10,11 @@ class DownloaderConfig {
ExtraConfig extra = ExtraConfig();
ProxyConfig proxy = ProxyConfig();
WebhookConfig webhook = WebhookConfig();
AutoTorrentConfig autoTorrent = AutoTorrentConfig();
ArchiveConfig archive = ArchiveConfig();
// New fields for auto-management
bool autoDeleteTorrents = false;
bool autoCleanMissingFiles = false;
bool autoDeleteMissingFileTasks = false;
DownloaderConfig({
this.downloadDir = '',
@@ -234,6 +234,22 @@ class ExtraConfigGithubMirror {
Map<String, dynamic> toJson() => _$ExtraConfigGithubMirrorToJson(this);
}
@JsonSerializable()
class AutoTorrentConfig {
bool enable;
bool deleteAfterDownload;
AutoTorrentConfig({
this.enable = false,
this.deleteAfterDownload = false,
});
factory AutoTorrentConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? AutoTorrentConfig() : _$AutoTorrentConfigFromJson(json);
Map<String, dynamic> toJson() => _$AutoTorrentConfigToJson(this);
}
@JsonSerializable()
class ArchiveConfig {
bool autoExtract;
@@ -248,4 +264,4 @@ class ArchiveConfig {
json == null ? ArchiveConfig() : _$ArchiveConfigFromJson(json);
Map<String, dynamic> toJson() => _$ArchiveConfigToJson(this);
}
}
@@ -17,10 +17,11 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>)
..webhook =
WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?)
..autoTorrent = AutoTorrentConfig.fromJson(
json['autoTorrent'] as Map<String, dynamic>?)
..archive =
ArchiveConfig.fromJson(json['archive'] as Map<String, dynamic>?)
..autoDeleteTorrents = json['autoDeleteTorrents'] as bool
..autoCleanMissingFiles = json['autoCleanMissingFiles'] as bool;
..autoDeleteMissingFileTasks = json['autoDeleteMissingFileTasks'] as bool;
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{
@@ -30,9 +31,9 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
'extra': instance.extra.toJson(),
'proxy': instance.proxy.toJson(),
'webhook': instance.webhook.toJson(),
'autoTorrent': instance.autoTorrent.toJson(),
'archive': instance.archive.toJson(),
'autoDeleteTorrents': instance.autoDeleteTorrents,
'autoCleanMissingFiles': instance.autoCleanMissingFiles,
'autoDeleteMissingFileTasks': instance.autoDeleteMissingFileTasks,
};
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
@@ -242,6 +243,18 @@ Map<String, dynamic> _$ExtraConfigGithubMirrorToJson(
'mirrors': instance.mirrors.map((e) => e.toJson()).toList(),
};
AutoTorrentConfig _$AutoTorrentConfigFromJson(Map<String, dynamic> json) =>
AutoTorrentConfig(
enable: json['enable'] as bool? ?? false,
deleteAfterDownload: json['deleteAfterDownload'] as bool? ?? false,
);
Map<String, dynamic> _$AutoTorrentConfigToJson(AutoTorrentConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
'deleteAfterDownload': instance.deleteAfterDownload,
};
ArchiveConfig _$ArchiveConfigFromJson(Map<String, dynamic> json) =>
ArchiveConfig(
autoExtract: json['autoExtract'] as bool? ?? true,
+6 -4
View File
@@ -25,15 +25,17 @@ class Options {
@JsonSerializable()
class OptsExtraHttp {
int connections;
bool autoTorrent;
bool autoExtract;
bool? autoTorrent;
bool? deleteTorrentAfterDownload;
bool? autoExtract;
String archivePassword;
bool deleteAfterExtract;
OptsExtraHttp({
this.connections = 0,
this.autoTorrent = false,
this.autoExtract = false,
this.autoTorrent,
this.deleteTorrentAfterDownload,
this.autoExtract,
this.archivePassword = '',
this.deleteAfterExtract = false,
});
+22 -10
View File
@@ -36,17 +36,29 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp(
connections: (json['connections'] as num?)?.toInt() ?? 0,
autoTorrent: json['autoTorrent'] as bool? ?? false,
autoExtract: json['autoExtract'] as bool? ?? false,
autoTorrent: json['autoTorrent'] as bool?,
deleteTorrentAfterDownload: json['deleteTorrentAfterDownload'] as bool?,
autoExtract: json['autoExtract'] as bool?,
archivePassword: json['archivePassword'] as String? ?? '',
deleteAfterExtract: json['deleteAfterExtract'] as bool? ?? false,
);
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{
'connections': instance.connections,
'autoTorrent': instance.autoTorrent,
'autoExtract': instance.autoExtract,
'archivePassword': instance.archivePassword,
'deleteAfterExtract': instance.deleteAfterExtract,
};
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) {
final val = <String, dynamic>{
'connections': instance.connections,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('autoTorrent', instance.autoTorrent);
writeNotNull(
'deleteTorrentAfterDownload', instance.deleteTorrentAfterDownload);
writeNotNull('autoExtract', instance.autoExtract);
val['archivePassword'] = instance.archivePassword;
val['deleteAfterExtract'] = instance.deleteAfterExtract;
return val;
}
@@ -26,7 +26,7 @@ class CreateController extends GetxController
@override
void onInit() {
super.onInit();
advancedTabController = TabController(length: 3, vsync: this);
advancedTabController = TabController(length: 2, vsync: this);
directDownload.value = Get.find<AppController>()
.downloaderConfig
.value
@@ -62,6 +62,8 @@ class CreateView extends GetView<CreateController> {
final _availableSchemes = ["http:", "https:", "magnet:"];
final _skipVerifyCertController = false.obs;
final _autoTorrentController = Rxn<bool>();
final _deleteTorrentAfterDownloadController = Rxn<bool>();
final _autoExtractController = Rxn<bool>();
final _deleteAfterExtractController = Rxn<bool>();
@@ -81,11 +83,6 @@ class CreateView extends GetView<CreateController> {
final downloadDir = appController.downloaderConfig.value.downloadDir;
_pathController.text = renderPathPlaceholders(downloadDir);
}
// Initialize archive settings from global config if not already set
_autoExtractController.value ??=
appController.downloaderConfig.value.archive.autoExtract;
_deleteAfterExtractController.value ??=
appController.downloaderConfig.value.archive.deleteAfterExtract;
// Handle pending create task from deep link
final CreateTask? routerParams = Get.rootDelegate.arguments();
@@ -509,20 +506,17 @@ class CreateView extends GetView<CreateController> {
const Divider(),
TabBar(
controller: controller.advancedTabController,
tabs: [
const Tab(
tabs: const [
Tab(
text: 'HTTP',
),
const Tab(
Tab(
text: 'BitTorrent',
),
Tab(
text: 'archives'.tr,
)
],
),
DefaultTabController(
length: 3,
length: 2,
child: ContentSizeTabBarView(
controller:
controller.advancedTabController,
@@ -605,21 +599,52 @@ class CreateView extends GetView<CreateController> {
),
),
),
],
),
Column(
children: [
TextFormField(
controller: _btTrackerController,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Trackers',
hintText: 'addTrackerHit'.tr,
)),
],
),
Column(
children: [
// AutoTorrent options
Padding(
padding:
const EdgeInsets.only(top: 10),
child: CompactCheckbox(
label: 'autoTorrentEnable'.tr,
value: _autoTorrentController
.value ??
false,
onChanged: (bool? value) {
_autoTorrentController.value =
value ?? false;
},
textStyle: const TextStyle(
color: Colors.grey,
),
),
),
Obx(
() => Visibility(
visible: _autoTorrentController
.value ??
false,
child: Padding(
padding: const EdgeInsets.only(
top: 10, left: 20),
child: CompactCheckbox(
label:
'autoTorrentDeleteAfterDownload'
.tr,
value:
_deleteTorrentAfterDownloadController
.value ??
false,
onChanged: (bool? value) {
_deleteTorrentAfterDownloadController
.value = value ?? false;
},
textStyle: const TextStyle(
color: Colors.grey,
),
),
),
),
),
// AutoExtract options
Padding(
padding:
const EdgeInsets.only(top: 10),
@@ -647,7 +672,7 @@ class CreateView extends GetView<CreateController> {
Padding(
padding:
const EdgeInsets.only(
top: 10),
top: 10, left: 20),
child: TextFormField(
controller:
_archivePasswordController,
@@ -666,7 +691,7 @@ class CreateView extends GetView<CreateController> {
Padding(
padding:
const EdgeInsets.only(
top: 10),
top: 10, left: 20),
child: CompactCheckbox(
label:
'deleteAfterExtract'
@@ -691,7 +716,18 @@ class CreateView extends GetView<CreateController> {
),
),
],
)
),
Column(
children: [
TextFormField(
controller: _btTrackerController,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Trackers',
hintText: 'addTrackerHit'.tr,
)),
],
),
],
),
)
@@ -892,8 +928,9 @@ class CreateView extends GetView<CreateController> {
Object? parseReqOptsExtra() {
return OptsExtraHttp()
..connections = int.tryParse(_connectionsController.text) ?? 0
..autoTorrent = true
..autoExtract = _autoExtractController.value ?? false
..autoTorrent = _autoTorrentController.value
..deleteTorrentAfterDownload = _deleteTorrentAfterDownloadController.value
..autoExtract = _autoExtractController.value
..archivePassword = _archivePasswordController.text
..deleteAfterExtract = _deleteAfterExtractController.value ?? false;
}
@@ -169,19 +169,19 @@ class SettingView extends GetView<SettingController> {
);
});
// New: Auto Delete Torrents
final buildAutoDeleteTorrents = _buildConfigItem('autoDeleteTorrents', () {
return appController.downloaderConfig.value.autoDeleteTorrents
// AutoTorrent: Enable auto create BT tasks from .torrent files
final buildAutoTorrentEnable = _buildConfigItem('autoTorrentEnable', () {
return appController.downloaderConfig.value.autoTorrent.enable
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value: appController.downloaderConfig.value.autoDeleteTorrents,
value: appController.downloaderConfig.value.autoTorrent.enable,
onChanged: (bool value) async {
appController.downloaderConfig.update((val) {
val!.autoDeleteTorrents = value;
val!.autoTorrent.enable = value;
});
await debounceSave();
},
@@ -189,19 +189,44 @@ class SettingView extends GetView<SettingController> {
);
});
// New: Auto Clean Missing Files
final buildAutoCleanMissingFiles = _buildConfigItem('autoCleanMissingFiles', () {
return appController.downloaderConfig.value.autoCleanMissingFiles
// AutoTorrent: Delete .torrent file after BT task creation
final buildAutoTorrentDeleteAfterDownload =
_buildConfigItem('autoTorrentDeleteAfterDownload', () {
return appController
.downloaderConfig.value.autoTorrent.deleteAfterDownload
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value: appController.downloaderConfig.value.autoCleanMissingFiles,
value: appController
.downloaderConfig.value.autoTorrent.deleteAfterDownload,
onChanged: (bool value) async {
appController.downloaderConfig.update((val) {
val!.autoCleanMissingFiles = value;
val!.autoTorrent.deleteAfterDownload = value;
});
await debounceSave();
},
),
);
});
// New: Auto Delete Missing File Tasks
final buildAutoDeleteMissingFileTasks =
_buildConfigItem('autoDeleteMissingFileTasks', () {
return appController.downloaderConfig.value.autoDeleteMissingFileTasks
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value:
appController.downloaderConfig.value.autoDeleteMissingFileTasks,
onChanged: (bool value) async {
appController.downloaderConfig.update((val) {
val!.autoDeleteMissingFileTasks = value;
});
await debounceSave();
},
@@ -1454,8 +1479,13 @@ class SettingView extends GetView<SettingController> {
buildMaxRunning(),
buildDefaultDirectDownload(),
buildAutoStartTasks(),
buildAutoDeleteTorrents(),
buildAutoCleanMissingFiles(),
buildAutoTorrentEnable(),
Obx(() => Visibility(
visible: appController.downloaderConfig.value
.autoTorrent.enable,
child: buildAutoTorrentDeleteAfterDownload(),
)),
buildAutoDeleteMissingFileTasks(),
buildBrowserExtension(),
buildAutoStartup(),
buildMenubarMode(),
@@ -1466,7 +1496,11 @@ class SettingView extends GetView<SettingController> {
child: Column(
children: _addDivider([
buildAutoExtract(),
buildDeleteAfterExtract(),
Obx(() => Visibility(
visible: appController.downloaderConfig.value
.archive.autoExtract,
child: buildDeleteAfterExtract(),
)),
]),
)),
const Text('HTTP'),
@@ -1990,4 +2024,4 @@ extension ProxyMode on ProxyConfig {
break;
}
}
}
}
+5 -2
View File
@@ -42,8 +42,11 @@ const deDE = {
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
'autoStartTasks': 'Unvollständige Aufgaben beim Start automatisch starten',
'autoDeleteTorrents': 'Automatisches Löschen von .torrent-Dateien',
'autoCleanMissingFiles': 'Automatisches Bereinigen fehlender Dateien',
'autoTorrentEnable':
'BT-Aufgaben automatisch aus .torrent-Dateien erstellen',
'autoTorrentDeleteAfterDownload':
'.torrent-Datei nach BT-Aufgabenerstellung löschen',
'autoDeleteMissingFileTasks': 'Fehlende Dateiaufgaben automatisch löschen',
'items': '@count Artikel',
'subscribeTracker': 'Abonnement-Tracker',
'subscribeFail':
+5 -3
View File
@@ -39,8 +39,10 @@ const enUS = {
'maxRunning': 'Max Running Tasks',
'defaultDirectDownload': 'Check direct download by default',
'autoStartTasks': 'Auto-start incomplete tasks on startup',
'autoDeleteTorrents': 'Auto delete .torrent files',
'autoCleanMissingFiles': 'Auto clean missing files',
'autoTorrentEnable': 'Auto create BT tasks from .torrent files',
'autoTorrentDeleteAfterDownload':
'Delete .torrent file after BT task creation',
'autoDeleteMissingFileTasks': 'Auto delete missing file tasks',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
@@ -174,4 +176,4 @@ const enUS = {
'githubMirrorUrl': 'Mirror URL',
'githubMirrorUrlHint': 'Enter mirror URL',
},
};
};
+6 -2
View File
@@ -39,8 +39,12 @@ const esES = {
'useServerCtime': 'Usar tiempo del servidor para la creación de archivos',
'maxRunning': 'Máximo de Tareas en Ejecución',
'autoStartTasks': 'Iniciar automáticamente tareas incompletas al arrancar',
'autoDeleteTorrents': 'Eliminar automáticamente archivos .torrent',
'autoCleanMissingFiles': 'Limpiar automáticamente archivos faltantes',
'autoTorrentEnable':
'Crear automáticamente tareas BT desde archivos .torrent',
'autoTorrentDeleteAfterDownload':
'Eliminar archivo .torrent después de crear tarea BT',
'autoDeleteMissingFileTasks':
'Eliminar automáticamente tareas con archivos faltantes',
'items': '@count elementos',
'subscribeTracker': 'Suscribirse al Tracker',
'subscribeFail':
+3 -2
View File
@@ -26,8 +26,9 @@ const faIR = {
'downloadDir': 'دایرکتوری دانلود',
'downloadDirValid': 'لطفا دایرکتوری دانلود را انتخاب کنید',
'connections': 'اتصالات',
'autoDeleteTorrents': 'حذف خودکار فایل‌های .torrent',
'autoCleanMissingFiles': 'پاکسازی خودکار فایل‌های گم شده',
'autoTorrentEnable': 'ایجاد خودکار وظایف BT از فایل‌های .torrent',
'autoTorrentDeleteAfterDownload': 'حذف فایل .torrent پس از ایجاد وظیفه BT',
'autoDeleteMissingFileTasks': 'حذف خودکار وظایف با فایل‌های گم شده',
'items': '@count آیتم ها',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
+6 -2
View File
@@ -40,8 +40,12 @@ const frFR = {
'maxRunning': 'Tâches en cours maximum',
'autoStartTasks':
'Démarrer automatiquement les tâches incomplètes au lancement',
'autoDeleteTorrents': 'Supprimer automatiquement les fichiers .torrent',
'autoCleanMissingFiles': 'Nettoyer automatiquement les fichiers manquants',
'autoTorrentEnable':
'Créer automatiquement des tâches BT à partir de fichiers .torrent',
'autoTorrentDeleteAfterDownload':
'Supprimer le fichier .torrent après création de tâche BT',
'autoDeleteMissingFileTasks':
'Supprimer automatiquement les tâches avec fichiers manquants',
'items': '@count éléments',
'subscribeTracker': 'S\'abonner au tracker',
'subscribeFail':
+6 -2
View File
@@ -40,8 +40,12 @@ const huHU = {
'maxRunning': 'Maximális futó feladatok',
'defaultDirectDownload':
'Alapértelmezettként ellenőrizze a közvetlen letöltést',
'autoDeleteTorrents': 'Automatikus .torrent fájlok törlése',
'autoCleanMissingFiles': 'Hiányzó fájlok automatikus tisztítása',
'autoTorrentEnable':
'BT feladatok automatikus létrehozása .torrent fájlokból',
'autoTorrentDeleteAfterDownload':
'.torrent fájl törlése a BT feladat létrehozása után',
'autoDeleteMissingFileTasks':
'Hiányzó fájlokkal rendelkező feladatok automatikus törlése',
'items': '@count elem',
'subscribeTracker': 'Nyomkövető előfizetése',
'subscribeFail':
+5 -2
View File
@@ -39,8 +39,11 @@ const idID = {
'useServerCtime': 'Gunakan waktu server untuk pembuatan file',
'maxRunning': 'Tugas Maksimum yang Berjalan',
'defaultDirectDownload': 'Pilih unduhan langsung sebagai opsi standar',
'autoDeleteTorrents': 'Otomatis hapus file .torrent',
'autoCleanMissingFiles': 'Otomatis bersihkan file yang hilang',
'autoTorrentEnable': 'Buat tugas BT secara otomatis dari file .torrent',
'autoTorrentDeleteAfterDownload':
'Hapus file .torrent setelah membuat tugas BT',
'autoDeleteMissingFileTasks':
'Otomatis hapus tugas dengan file yang hilang',
'items': '@count items',
'subscribeTracker': 'Berlangganan Tracker',
'subscribeFail':
+5 -2
View File
@@ -37,8 +37,11 @@ const itIT = {
'connections': 'Connessioni',
'useServerCtime': "Utilizza l'ora del server per la creazione dei file",
'maxRunning': 'Numero massimo di attività in esecuzione',
'autoDeleteTorrents': 'Elimina automaticamente i file .torrent',
'autoCleanMissingFiles': 'Pulisci automaticamente i file mancanti',
'autoTorrentEnable': 'Crea automaticamente attività BT da file .torrent',
'autoTorrentDeleteAfterDownload':
'Elimina file .torrent dopo creazione attività BT',
'autoDeleteMissingFileTasks':
'Elimina automaticamente le attività con file mancanti',
'items': '@count elementi',
'subscribeTracker': 'Iscriviti al tracker',
'subscribeFail':
+3 -2
View File
@@ -30,8 +30,9 @@ const jaJP = {
'connections': '接続',
'maxRunning': '最大実行タスク',
'autoStartTasks': '起動時に未完了のタスクを自動的に開始',
'autoDeleteTorrents': '.torrentファイルを自動削除',
'autoCleanMissingFiles': '欠落ファイルを自動クリーン',
'autoTorrentEnable': '.torrentファイルからBTタスクを自動作成',
'autoTorrentDeleteAfterDownload': 'BTタスク作成後に.torrentファイルを削除',
'autoDeleteMissingFileTasks': '欠落ファイルタスクを自動削除',
'items': '@count アイテム',
'subscribeTracker': 'トラッカーを購読',
'subscribeFail': '登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください',
+5 -2
View File
@@ -36,8 +36,11 @@ const plPL = {
'connections': 'Połączenia',
'useServerCtime': 'Użyj czasu serwera do utworzenia pliku',
'maxRunning': 'Maksymalna liczba zadań',
'autoDeleteTorrents': 'Automatycznie usuń pliki .torrent',
'autoCleanMissingFiles': 'Automatycznie wyczyść brakujące pliki',
'autoTorrentEnable': 'Automatyczne tworzenie zadań BT z plików .torrent',
'autoTorrentDeleteAfterDownload':
'Usuń plik .torrent po utworzeniu zadania BT',
'autoDeleteMissingFileTasks':
'Automatycznie usuwaj zadania z brakującymi plikami',
'items': '@count items',
'subscribeTracker': 'Subskrybuj tracker',
'subscribeFail': 'Błąd subskrypcji, sprawdź łączność z internetem',
+6 -2
View File
@@ -39,8 +39,12 @@ const ptBR = {
'useServerCtime': 'Use server time for file creation',
'maxRunning': 'Máximo de Tarefas Simultâneas',
'defaultDirectDownload': 'Marcar download direto por padrão',
'autoDeleteTorrents': 'Excluir automaticamente arquivos .torrent',
'autoCleanMissingFiles': 'Limpar automaticamente arquivos ausentes',
'autoTorrentEnable':
'Criar automaticamente tarefas BT de arquivos .torrent',
'autoTorrentDeleteAfterDownload':
'Excluir arquivo .torrent após criação da tarefa BT',
'autoDeleteMissingFileTasks':
'Excluir automaticamente tarefas com arquivos ausentes',
'items': '@count itens',
'subscribeTracker': 'Assinar Rastreador',
'subscribeFail':
+5 -3
View File
@@ -40,8 +40,10 @@ const ruRU = {
'maxRunning': 'Максимальное количество активных задач',
'defaultDirectDownload': 'Прямая загрузка по умолчанию',
'autoStartTasks': 'Автоматически запускать незавершённые задачи при старте',
'autoDeleteTorrents': 'Автоудаление торрент-файлов',
'autoCleanMissingFiles': 'Автоочистка отсутствующих файлов',
'autoTorrentEnable': 'Автоматически создавать BT-задачи из .torrent файлов',
'autoTorrentDeleteAfterDownload':
'Удалить .torrent файл после создания BT-задачи',
'autoDeleteMissingFileTasks': 'Автоудаление задач с отсутствующими файлами',
'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера',
'subscribeFail':
@@ -151,4 +153,4 @@ const ruRU = {
'builtInCategory': 'Встроенную категорию нельзя удалить',
'selectCategory': 'Выбрать категорию',
}
};
};
+5 -2
View File
@@ -37,8 +37,11 @@ const taTA = {
'connections': 'தொடர்புகள்',
'useServerCtime': 'Use server time for file creation',
'maxRunning': 'அதிகபட்ச இயங்கும் பணிகள்',
'autoDeleteTorrents': 'தானாக .torrent கோப்புகளை நீக்கு',
'autoCleanMissingFiles': 'காணாமல் போன கோப்புகளை தானாக சுத்தம் செய்',
'autoTorrentEnable': '.torrent கோப்புகளிலிருந்து BT பணிகளை தானாக உருவாக்கு',
'autoTorrentDeleteAfterDownload':
'BT பணி உருவாக்கத்திற்குப் பிறகு .torrent கோப்பை நீக்கு',
'autoDeleteMissingFileTasks':
'காணாமல் போன கோப்புகளுடன் பணிகளை தானாக நீக்கு',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
+4 -2
View File
@@ -39,8 +39,10 @@ const trTR = {
'useServerCtime': 'Dosya oluşturma için sunucu zamanını kullan',
'maxRunning': 'Maksimum Çalışan Görev',
'defaultDirectDownload': 'Varsayılan olarak doğrudan indirmeyi seç',
'autoDeleteTorrents': '.torrent dosyalarını otomatik sil',
'autoCleanMissingFiles': 'Eksik dosyaları otomatik temizle',
'autoTorrentEnable': '.torrent dosyalarından otomatik BT görevi oluştur',
'autoTorrentDeleteAfterDownload':
'BT görevi oluşturulduktan sonra .torrent dosyasını sil',
'autoDeleteMissingFileTasks': 'Eksik dosya görevlerini otomatik sil',
'items': '@count öğe',
'subscribeTracker': 'Tracker Aboneliği',
'subscribeFail':
+5 -2
View File
@@ -39,8 +39,11 @@ const ukUA = {
'useServerCtime': 'Використовувати час сервера для створення файлу',
'maxRunning': 'Макс. завдань у черзі',
'defaultDirectDownload': 'Пряме завантаження за замовчуванням',
'autoDeleteTorrents': 'Автоматично видаляти файли .torrent',
'autoCleanMissingFiles': 'Автоматично очищати відсутні файли',
'autoTorrentEnable': 'Автоматично створювати BT-завдання з .torrent файлів',
'autoTorrentDeleteAfterDownload':
'Видалити .torrent файл після створення BT-завдання',
'autoDeleteMissingFileTasks':
'Автоматично видаляти завдання з відсутніми файлами',
'items': '@count елементів',
'subscribeTracker': 'Підписатися на трекер',
'subscribeFail':
+4 -2
View File
@@ -36,8 +36,10 @@ const viVN = {
'connections': 'Kết nối',
'useServerCtime': 'Sử dụng thời gian máy chủ cho việc tạo tệp',
'maxRunning': 'Số nhiệm vụ tối đa',
'autoDeleteTorrents': 'Tự động xóa tệp .torrent',
'autoCleanMissingFiles': 'Tự động dọn dẹp tệp bị thiếu',
'autoTorrentEnable': 'Tự động tạo nhiệm vụ BT từ file .torrent',
'autoTorrentDeleteAfterDownload':
'Xóa file .torrent sau khi tạo nhiệm vụ BT',
'autoDeleteMissingFileTasks': 'Tự động xóa tác vụ có tệp bị thiếu',
'items': '@count mục',
'subscribeTracker': 'Theo dõi Tracker',
'subscribeFail':
+3 -2
View File
@@ -39,8 +39,9 @@ const zhCN = {
'maxRunning': '最大下载数',
'defaultDirectDownload': '默认勾选直接下载',
'autoStartTasks': '启动时自动开始未完成的任务',
'autoDeleteTorrents': '自动删除 .torrent 文件',
'autoCleanMissingFiles': '自动清理缺失文件',
'autoTorrentEnable': '自动从 .torrent 文件创建 BT 任务',
'autoTorrentDeleteAfterDownload': '创建 BT 任务后删除 .torrent 文件',
'autoDeleteMissingFileTasks': '自动删除缺失文件任务',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
+3 -2
View File
@@ -39,8 +39,9 @@ const zhTW = {
'maxRunning': '最大執行任務數',
'defaultDirectDownload': '預設勾選直接下載',
'autoStartTasks': '啟動時自動開始未完成的任務',
'autoDeleteTorrents': '自動刪除 .torrent 檔案',
'autoCleanMissingFiles': '自動清理缺失檔案',
'autoTorrentEnable': '自動從 .torrent 檔案建立 BT 任務',
'autoTorrentDeleteAfterDownload': '建立 BT 任務後刪除 .torrent 檔案',
'autoDeleteMissingFileTasks': '自動刪除缺失檔案任務',
'items': '@count 個項目',
'subscribeTracker': '訂閱 Tracker',
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',