feat(app): add internal components batch 1

This commit is contained in:
吴璨
2026-09-30 10:19:23 +08:00
parent b094d0a6b1
commit a9d79bbe23
10 changed files with 1927 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
package app
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
)
type runtimeSettings struct {
Concurrency int `json:"concurrency"`
RequestConcurrency int `json:"requestConcurrency"`
RequestIntervalMS int `json:"requestIntervalMs"`
ProxyURL *string `json:"proxyURL,omitempty"`
OutputDir *string `json:"outputDir,omitempty"`
GroupBySource *bool `json:"groupBySource,omitempty"`
}
func (settings runtimeSettings) validate() error {
if settings.OutputDir != nil {
if _, err := normalizedOutputDirectory(*settings.OutputDir); err != nil {
return err
}
}
if settings.ProxyURL != nil {
if _, err := configuredProxy(*settings.ProxyURL); err != nil {
return err
}
}
if settings.Concurrency < 1 || settings.Concurrency > 32 {
return errors.New("下载并发范围为 1–32")
}
if settings.RequestConcurrency < 1 || settings.RequestConcurrency > 16 {
return errors.New("剧库请求并发范围为 1–16")
}
if settings.RequestIntervalMS < 100 || settings.RequestIntervalMS > 60000 {
return errors.New("同站请求间隔范围为 100–60000 毫秒")
}
return nil
}
func runtimeSettingsPath(directory string) string {
return filepath.Join(directory, "juku.json")
}
func loadRuntimeSettings(cfg *Config) {
if cfg.settingsLoaded {
return
}
cfg.settingsLoaded = true
document, err := readConfigDocument(runtimeSettingsPath(cfg.runtimeSettingsDirectory()))
if err == nil {
loaded, configErr := document.config()
if configErr == nil {
loaded.dataDir = cfg.dataDirectory()
loaded.settingsLoaded = true
*cfg = loaded
}
return
}
if !errors.Is(err, os.ErrNotExist) {
fmt.Printf("配置读取失败,保留当前设置: %v\n", err)
return
}
body, err := os.ReadFile(filepath.Join(cfg.OutputDir, "settings.json"))
if errors.Is(err, os.ErrNotExist) {
return
}
var settings runtimeSettings
if err == nil {
err = json.Unmarshal(body, &settings)
}
if err == nil {
err = settings.validate()
}
if err != nil {
fmt.Printf(" 界面并发设置读取失败,使用配置文件/默认值: %v\n", err)
return
}
applyRuntimeSettings(cfg, settings)
}
func applyRuntimeSettings(cfg *Config, settings runtimeSettings) {
cfg.Concurrency = settings.Concurrency
cfg.RequestConcurrency = settings.RequestConcurrency
cfg.RequestIntervalMS = settings.RequestIntervalMS
if settings.ProxyURL != nil {
cfg.ProxyURL = *settings.ProxyURL
}
if settings.OutputDir != nil {
cfg.OutputDir, _ = normalizedOutputDirectory(*settings.OutputDir)
}
if settings.GroupBySource != nil {
cfg.GroupBySource = *settings.GroupBySource
}
}
func (a *UIApp) updateRuntimeSettings(writer http.ResponseWriter, request *http.Request) bool {
var settings runtimeSettings
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 4096))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&settings); err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "无效的并发设置"})
return false
}
if err := settings.validate(); err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": err.Error()})
return false
}
a.settingsMu.Lock()
defer a.settingsMu.Unlock()
a.mu.Lock()
cfg := a.cfg
if settings.ProxyURL == nil {
value := a.cfg.ProxyURL
settings.ProxyURL = &value
}
if settings.OutputDir == nil {
value := firstNonEmpty(a.nextOutputDir, cfg.outputDirSetting, cfg.OutputDir)
settings.OutputDir = &value
}
if settings.GroupBySource == nil {
value := cfg.GroupBySource
settings.GroupBySource = &value
}
a.mu.Unlock()
directory, err := normalizedOutputDirectory(*settings.OutputDir)
if err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": err.Error()})
return false
}
settings.OutputDir = &directory
if !sameDirectory(directory, cfg.OutputDir) {
if err := checkOutputDirectory(directory); err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": a.redactError(err)})
return false
}
}
if err := saveRuntimeSettings(cfg.runtimeSettingsDirectory(), settings); err != nil {
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "设置保存失败: " + a.redactError(err)})
return false
}
a.downloader.proxyRouter.configure(*settings.ProxyURL)
a.downloader.client.CloseIdleConnections()
a.mu.Lock()
a.cfg.Concurrency = settings.Concurrency
a.cfg.RequestConcurrency = settings.RequestConcurrency
a.cfg.RequestIntervalMS = settings.RequestIntervalMS
a.cfg.ProxyURL = *settings.ProxyURL
a.cfg.GroupBySource = *settings.GroupBySource
a.downloader.directoryMu.Lock()
a.downloader.downloadGrouping = settings.GroupBySource
a.downloader.directoryMu.Unlock()
a.nextOutputDir = directory
a.downloader.limiter.configure(settings.RequestConcurrency, time.Duration(settings.RequestIntervalMS)*time.Millisecond)
a.ensureWorkersLocked()
a.cond.Broadcast()
a.mu.Unlock()
return true
}
func saveRuntimeSettings(directory string, settings runtimeSettings) error {
if err := settings.validate(); err != nil {
return err
}
path := runtimeSettingsPath(directory)
document, err := readConfigDocument(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
document.Download.Concurrency = settings.Concurrency
document.Network.RequestConcurrency = settings.RequestConcurrency
document.Network.RequestIntervalMS = settings.RequestIntervalMS
if settings.OutputDir != nil {
document.Download.Directory = *settings.OutputDir
}
if settings.GroupBySource != nil {
document.Download.GroupBySource = *settings.GroupBySource
}
if settings.ProxyURL != nil {
document.Network.Proxy = *settings.ProxyURL
}
return writeConfigDocument(path, document)
}