Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d654368290 |
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"caorushizi.cn/mediago/internal/app"
|
||||
"caorushizi.cn/mediago/internal/core"
|
||||
"caorushizi.cn/mediago/internal/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := newRootCommand().Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func newRootCommand() *cobra.Command {
|
||||
cfg := app.DefaultConfig()
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "mediago-cli",
|
||||
Short: "MediaGo command-line interface",
|
||||
Long: "MediaGo CLI directly uses the core downloader runtime without going through the HTTP API server.",
|
||||
}
|
||||
addConfigFlags(rootCmd.PersistentFlags(), cfg)
|
||||
rootCmd.AddCommand(newDownloadCommand(cfg))
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
func newDownloadCommand(cfg *app.AppConfig) *cobra.Command {
|
||||
var id string
|
||||
var typ string
|
||||
var name string
|
||||
var folder string
|
||||
var headers []string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "download <url>",
|
||||
Short: "Download a URL directly with MediaGo core",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg.ApplyEnvAndDefaults()
|
||||
if err := app.InitLogger(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
if id == "" {
|
||||
id = uuid.New().String()
|
||||
}
|
||||
if name == "" {
|
||||
name = "download-" + uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
rt, err := app.NewRuntime(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rt.Close()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
params := core.DownloadParams{
|
||||
ID: core.TaskID(id),
|
||||
Type: core.DownloadType(typ),
|
||||
URL: args[0],
|
||||
Name: core.SanitizeFilename(name),
|
||||
Folder: folder,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
lastPercent := -1
|
||||
fmt.Printf("Starting %s download: %s\n", params.Type, params.Name)
|
||||
err = rt.Downloader.Download(ctx, params, core.Callbacks{
|
||||
OnProgress: func(e core.ProgressEvent) {
|
||||
percent := int(e.Percent)
|
||||
if percent != lastPercent {
|
||||
lastPercent = percent
|
||||
if e.Speed != "" {
|
||||
fmt.Printf("\r%3d%% %s", percent, e.Speed)
|
||||
} else {
|
||||
fmt.Printf("\r%3d%%", percent)
|
||||
}
|
||||
}
|
||||
},
|
||||
OnMessage: func(e core.MessageEvent) {
|
||||
if strings.TrimSpace(e.Message) != "" {
|
||||
logger.Debugf("[%s] %s", e.ID, e.Message)
|
||||
}
|
||||
},
|
||||
})
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Download completed")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&id, "id", "", "Download task id")
|
||||
cmd.Flags().StringVarP(&typ, "type", "t", string(core.TypeM3U8), "Download type (m3u8, bilibili, direct, mediago, youtube)")
|
||||
cmd.Flags().StringVarP(&name, "name", "n", "", "Output file name")
|
||||
cmd.Flags().StringVar(&folder, "folder", "", "Subdirectory under the download directory")
|
||||
cmd.Flags().StringArrayVarP(&headers, "header", "H", nil, "HTTP header, can be repeated")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addConfigFlags(flags *pflag.FlagSet, cfg *app.AppConfig) {
|
||||
flags.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (debug/info/warn/error)")
|
||||
flags.StringVar(&cfg.LogDir, "log-dir", cfg.LogDir, "Log directory")
|
||||
flags.StringVar(&cfg.DepsDir, "deps-dir", cfg.DepsDir, "Directory containing downloader tool binaries")
|
||||
flags.StringVar(&cfg.SchemaPath, "schema-path", cfg.SchemaPath, "Path to the download schema config.json")
|
||||
flags.StringVar(&cfg.LocalDir, "local-dir", cfg.LocalDir, "Default download directory")
|
||||
flags.BoolVar(&cfg.DeleteSegments, "delete-segments", cfg.DeleteSegments, "Delete segments after download")
|
||||
flags.StringVar(&cfg.Proxy, "proxy", cfg.Proxy, "Proxy for downloader")
|
||||
flags.BoolVar(&cfg.UseProxy, "use-proxy", cfg.UseProxy, "Enable proxy")
|
||||
flags.IntVar(&cfg.MaxRunner, "max-runner", cfg.MaxRunner, "Maximum concurrent download runners")
|
||||
flags.StringVar(&cfg.DBPath, "db-path", cfg.DBPath, "Path to SQLite database file")
|
||||
flags.StringVar(&cfg.ConfigDir, "config-dir", cfg.ConfigDir, "Directory for persistent config file")
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package main
|
||||
|
||||
// AppStore holds all user-facing configuration options.
|
||||
// Field names and defaults match the TypeScript AppStore interface
|
||||
// in @mediago/shared-common.
|
||||
type AppStore struct {
|
||||
Local string `json:"local"`
|
||||
PromptTone bool `json:"promptTone"`
|
||||
Proxy string `json:"proxy"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
DeleteSegments bool `json:"deleteSegments"`
|
||||
OpenInNewWindow bool `json:"openInNewWindow"`
|
||||
BlockAds bool `json:"blockAds"`
|
||||
Theme string `json:"theme"`
|
||||
UseExtension bool `json:"useExtension"`
|
||||
IsMobile bool `json:"isMobile"`
|
||||
MaxRunner int `json:"maxRunner"`
|
||||
Language string `json:"language"`
|
||||
ShowTerminal bool `json:"showTerminal"`
|
||||
Privacy bool `json:"privacy"`
|
||||
MachineId string `json:"machineId"`
|
||||
DownloadProxySwitch bool `json:"downloadProxySwitch"`
|
||||
AutoUpgrade bool `json:"autoUpgrade"`
|
||||
AllowBeta bool `json:"allowBeta"`
|
||||
CloseMainWindow bool `json:"closeMainWindow"`
|
||||
AudioMuted bool `json:"audioMuted"`
|
||||
EnableDocker bool `json:"enableDocker"`
|
||||
DockerUrl string `json:"dockerUrl"`
|
||||
EnableMobilePlayer bool `json:"enableMobilePlayer"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
}
|
||||
|
||||
// defaultAppStore returns default config values matching the TS appStoreDefaults.
|
||||
func defaultAppStore() AppStore {
|
||||
return AppStore{
|
||||
Local: "",
|
||||
PromptTone: true,
|
||||
Proxy: "",
|
||||
UseProxy: false,
|
||||
DeleteSegments: true,
|
||||
OpenInNewWindow: false,
|
||||
BlockAds: true,
|
||||
Theme: "system",
|
||||
UseExtension: false,
|
||||
IsMobile: false,
|
||||
MaxRunner: 2,
|
||||
Language: "system",
|
||||
ShowTerminal: false,
|
||||
Privacy: false,
|
||||
MachineId: "",
|
||||
DownloadProxySwitch: false,
|
||||
AutoUpgrade: true,
|
||||
AllowBeta: false,
|
||||
CloseMainWindow: false,
|
||||
AudioMuted: true,
|
||||
EnableDocker: false,
|
||||
DockerUrl: "",
|
||||
EnableMobilePlayer: false,
|
||||
ApiKey: "",
|
||||
PasswordHash: "",
|
||||
}
|
||||
}
|
||||
+47
-352
@@ -3,21 +3,15 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"caorushizi.cn/mediago/internal/api"
|
||||
"caorushizi.cn/mediago/internal/api/handler"
|
||||
"caorushizi.cn/mediago/internal/core"
|
||||
"caorushizi.cn/mediago/internal/core/runner"
|
||||
"caorushizi.cn/mediago/internal/core/schema"
|
||||
"caorushizi.cn/mediago/internal/db"
|
||||
"caorushizi.cn/mediago/internal/app"
|
||||
"caorushizi.cn/mediago/internal/logger"
|
||||
"caorushizi.cn/mediago/internal/tasklog"
|
||||
"caorushizi.cn/mediago/pkg/conf"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -48,227 +42,64 @@ import (
|
||||
// @tag.name Events
|
||||
// @tag.description Real-time event streaming endpoints
|
||||
|
||||
// AppConfig stores all startup configuration options (passed via command-line flags or environment variables).
|
||||
type AppConfig struct {
|
||||
GinMode string `json:"gin_mode"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
LogLevel string `json:"log_level"`
|
||||
LogDir string `json:"log_dir"`
|
||||
SchemaPath string `json:"schema_path"`
|
||||
DepsDir string `json:"deps_dir"`
|
||||
MaxRunner int `json:"max_runner"`
|
||||
LocalDir string `json:"local_dir"`
|
||||
DeleteSegments bool `json:"delete_segments"`
|
||||
Proxy string `json:"proxy"`
|
||||
UseProxy bool `json:"use_proxy"`
|
||||
DBPath string `json:"db_path"`
|
||||
ConfigDir string `json:"config_dir"`
|
||||
EnableAuth bool `json:"enable_auth"`
|
||||
StaticDir string `json:"static_dir"`
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetLocalDir() string {
|
||||
return c.LocalDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetDeleteSegments() bool {
|
||||
return c.DeleteSegments
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetProxy() string {
|
||||
return c.Proxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUseProxy() bool {
|
||||
return c.UseProxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetLocalDir(dir string) {
|
||||
c.LocalDir = dir
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetDeleteSegments(del bool) {
|
||||
c.DeleteSegments = del
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetProxy(proxy string) {
|
||||
c.Proxy = proxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetUseProxy(useProxy bool) {
|
||||
c.UseProxy = useProxy
|
||||
}
|
||||
|
||||
// getSystemDownloadsDir returns the system downloads directory.
|
||||
// Prefers $HOME/Downloads; falls back to $HOME if it does not exist.
|
||||
func getSystemDownloadsDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
downloads := filepath.Join(home, "Downloads")
|
||||
if info, err := os.Stat(downloads); err == nil && info.IsDir() {
|
||||
return downloads
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 1. Initialize the logger with default config first so it is available during config parsing
|
||||
// if err := logger.Init(logger.DefaultConfig()); err != nil {
|
||||
// panic("Failed to initialize logger: " + err.Error())
|
||||
// }
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Initialize and parse configuration
|
||||
cfg := initConfig()
|
||||
func run(args []string) error {
|
||||
cfg := app.DefaultConfig()
|
||||
fs := flag.NewFlagSet("mediago-core server", flag.ContinueOnError)
|
||||
registerConfigFlags(fs, cfg)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
cfg.ApplyEnvAndDefaults()
|
||||
|
||||
// 3. Re-initialize the logger using the resolved configuration
|
||||
logCfg := logger.DefaultConfig()
|
||||
logCfg.Level = cfg.LogLevel
|
||||
logCfg.LogDir = cfg.LogDir
|
||||
|
||||
if err := logger.Init(logCfg); err != nil {
|
||||
logger.Fatalf("Failed to reinitialize logger with config: %v", err)
|
||||
if err := app.InitLogger(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("MediaGo Downloader Service Starting...")
|
||||
logger.Infof("Final Config: %+v", cfg)
|
||||
|
||||
// 4. Initialize AppStore configuration (user-level persistent config)
|
||||
appStore, err := conf.New(conf.Options[AppStore]{
|
||||
ConfigName: "config",
|
||||
CWD: cfg.ConfigDir,
|
||||
Defaults: defaultAppStore(),
|
||||
})
|
||||
rt, err := app.NewRuntime(cfg)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to initialize app store: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("App store initialized at: %s", appStore.Path())
|
||||
defer rt.Close()
|
||||
|
||||
// Ensure machineId is set (generate on first run)
|
||||
if appStore.Store().MachineId == "" {
|
||||
newId := uuid.New().String()
|
||||
_ = appStore.Set("machineId", newId)
|
||||
logger.Infof("Generated new machineId: %s", newId)
|
||||
}
|
||||
return runServer(rt)
|
||||
}
|
||||
|
||||
// Sync appStore values back into cfg (so cfg reflects the final state of persistent config).
|
||||
// Note: CLI arguments are no longer written into appStore to avoid overwriting settings
|
||||
// saved by the user in the UI on every startup. CLI args serve only as initial defaults;
|
||||
// appStore (user config) takes higher priority.
|
||||
syncAppStoreToCfg(appStore, cfg)
|
||||
func registerConfigFlags(fs *flag.FlagSet, cfg *app.AppConfig) {
|
||||
fs.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (debug/info/warn/error)")
|
||||
fs.StringVar(&cfg.LogDir, "log-dir", cfg.LogDir, "Log directory")
|
||||
fs.StringVar(&cfg.DepsDir, "deps-dir", cfg.DepsDir, "Directory containing downloader tool binaries")
|
||||
fs.StringVar(&cfg.SchemaPath, "schema-path", cfg.SchemaPath, "Path to the download schema config.json")
|
||||
fs.StringVar(&cfg.Port, "port", cfg.Port, "Server port")
|
||||
fs.StringVar(&cfg.LocalDir, "local-dir", cfg.LocalDir, "Default download directory")
|
||||
fs.BoolVar(&cfg.DeleteSegments, "delete-segments", cfg.DeleteSegments, "Delete segments after download")
|
||||
fs.StringVar(&cfg.Proxy, "proxy", cfg.Proxy, "Proxy for downloader")
|
||||
fs.BoolVar(&cfg.UseProxy, "use-proxy", cfg.UseProxy, "Enable proxy")
|
||||
fs.IntVar(&cfg.MaxRunner, "max-runner", cfg.MaxRunner, "Maximum concurrent download runners")
|
||||
fs.StringVar(&cfg.DBPath, "db-path", cfg.DBPath, "Path to SQLite database file")
|
||||
fs.StringVar(&cfg.ConfigDir, "config-dir", cfg.ConfigDir, "Directory for persistent config file")
|
||||
fs.BoolVar(&cfg.EnableAuth, "enable-auth", cfg.EnableAuth, "Enable API key authentication")
|
||||
fs.StringVar(&cfg.StaticDir, "static-dir", cfg.StaticDir, "Directory to serve static files from (SPA mode)")
|
||||
}
|
||||
|
||||
// If the download directory is empty, is the default value, or does not exist, use the system downloads directory
|
||||
{
|
||||
needDefault := cfg.LocalDir == "" || cfg.LocalDir == "./downloads"
|
||||
if !needDefault {
|
||||
if info, err := os.Stat(cfg.LocalDir); err != nil || !info.IsDir() {
|
||||
needDefault = true
|
||||
}
|
||||
}
|
||||
if needDefault {
|
||||
sysDownloads := getSystemDownloadsDir()
|
||||
logger.Infof("Download dir %q unavailable, using system default: %s", cfg.LocalDir, sysDownloads)
|
||||
cfg.LocalDir = sysDownloads
|
||||
}
|
||||
// Ensure appStore has the download directory (so UI can read it via /api/config)
|
||||
if appStore.Store().Local == "" {
|
||||
_ = appStore.Set("local", cfg.LocalDir)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Load JSON Schema configuration
|
||||
logger.Infof("Loading schemas from: %s", cfg.SchemaPath)
|
||||
schemas, err := schema.LoadSchemasFromJSON(cfg.SchemaPath)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to load schemas: %v", err)
|
||||
}
|
||||
logger.Infof("Loaded %d download schemas", len(schemas.Schemas))
|
||||
|
||||
// 6. Configure downloader binary paths
|
||||
binMap := getBinaryMap(cfg)
|
||||
for dt, binPath := range binMap {
|
||||
logger.Infof("%s downloader: %s", dt, binPath)
|
||||
if binPath == "" {
|
||||
continue
|
||||
}
|
||||
if info, err := os.Stat(binPath); err != nil {
|
||||
logger.Warnf("%s binary not found: %v", dt, err)
|
||||
} else if info.Mode()&0o111 == 0 {
|
||||
logger.Warnf("%s binary is not executable: %s", dt, binPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Create core components
|
||||
r := runner.NewPTYRunner()
|
||||
downloader := core.NewDownloader(binMap, r, schemas, cfg)
|
||||
queue := core.NewTaskQueue(downloader, cfg.MaxRunner)
|
||||
taskLogs := tasklog.NewManager(filepath.Join(cfg.LogDir, "tasks"))
|
||||
|
||||
logger.Infof("Task queue initialized (maxRunner=%d)", cfg.MaxRunner)
|
||||
logger.Infof("Task logs will be stored in %s", filepath.Join(cfg.LogDir, "tasks"))
|
||||
|
||||
// 8. Watch appStore changes and sync them to the downloader
|
||||
appStore.OnDidChange("maxRunner", func(newVal, oldVal any) {
|
||||
if v, ok := toInt(newVal); ok {
|
||||
queue.SetMaxRunner(v)
|
||||
logger.Infof("maxRunner updated to %d via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("proxy", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(string); ok {
|
||||
cfg.SetProxy(v)
|
||||
logger.Infof("proxy updated to %q via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("useProxy", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(bool); ok {
|
||||
cfg.SetUseProxy(v)
|
||||
logger.Infof("useProxy updated to %v via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("deleteSegments", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(bool); ok {
|
||||
cfg.SetDeleteSegments(v)
|
||||
logger.Infof("deleteSegments updated to %v via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("local", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(string); ok {
|
||||
cfg.SetLocalDir(v)
|
||||
logger.Infof("localDir updated to %q via config change", v)
|
||||
}
|
||||
})
|
||||
|
||||
// 9. Initialize database (optional)
|
||||
var database *db.Database
|
||||
if cfg.DBPath != "" {
|
||||
// Ensure the database parent directory exists
|
||||
if dir := filepath.Dir(cfg.DBPath); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
logger.Fatalf("Failed to create database directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
var dbErr error
|
||||
database, dbErr = db.New(cfg.DBPath)
|
||||
if dbErr != nil {
|
||||
logger.Fatalf("Failed to open database: %v", dbErr)
|
||||
}
|
||||
defer database.Close()
|
||||
logger.Infof("Database opened: %s", cfg.DBPath)
|
||||
} else {
|
||||
logger.Info("No database path provided, running without persistence")
|
||||
}
|
||||
|
||||
// 10. Start HTTP server
|
||||
confStore := handler.WrapConfStore[AppStore](appStore)
|
||||
func runServer(rt *app.Runtime) error {
|
||||
cfg := rt.Config
|
||||
confStore := handler.WrapConfStore[app.AppStore](rt.AppStore)
|
||||
execPath, _ := os.Executable()
|
||||
server := api.NewServer(queue, taskLogs, database, confStore, api.ServerOptions{
|
||||
server := api.NewServer(rt.Queue, rt.TaskLogs, rt.Database, confStore, api.ServerOptions{
|
||||
EnableAuth: cfg.EnableAuth,
|
||||
StaticDir: cfg.StaticDir,
|
||||
FFmpegBin: getFFmpegBin(cfg),
|
||||
FFmpegBin: app.FFmpegBinPath(cfg),
|
||||
VideoRoot: cfg.LocalDir,
|
||||
EnvPaths: handler.EnvPaths{
|
||||
ConfigDir: cfg.ConfigDir,
|
||||
@@ -280,141 +111,5 @@ func main() {
|
||||
gin.SetMode(cfg.GinMode)
|
||||
logger.Infof("Starting HTTP server on %s", addr)
|
||||
|
||||
if err := server.Run(addr); err != nil {
|
||||
logger.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// syncAppStoreToCfg reads appStore values back into cfg.
|
||||
// CLI flags take priority: if --local-dir was explicitly set (not the default),
|
||||
// the CLI value is kept and written back to appStore.
|
||||
func syncAppStoreToCfg(store *conf.Conf[AppStore], cfg *AppConfig) {
|
||||
s := store.Store()
|
||||
cliLocalDir := cfg.LocalDir
|
||||
cliExplicit := cliLocalDir != "" && cliLocalDir != "./downloads"
|
||||
|
||||
if cliExplicit {
|
||||
// CLI explicitly set --local-dir, use it and update appStore
|
||||
_ = store.Set("local", cliLocalDir)
|
||||
} else if s.Local != "" {
|
||||
cfg.LocalDir = s.Local
|
||||
}
|
||||
cfg.Proxy = s.Proxy
|
||||
cfg.UseProxy = s.UseProxy
|
||||
cfg.DeleteSegments = s.DeleteSegments
|
||||
if s.MaxRunner > 0 {
|
||||
cfg.MaxRunner = s.MaxRunner
|
||||
}
|
||||
}
|
||||
|
||||
// initConfig initializes configuration following priority order: CLI flags > environment variables > JSON string > defaults.
|
||||
func initConfig() *AppConfig {
|
||||
// Default configuration
|
||||
cfg := &AppConfig{
|
||||
GinMode: "release",
|
||||
Host: "0.0.0.0",
|
||||
Port: "8080",
|
||||
LogLevel: "info",
|
||||
LogDir: "./logs",
|
||||
SchemaPath: "", // computed later
|
||||
DepsDir: "",
|
||||
MaxRunner: 2,
|
||||
LocalDir: "./downloads",
|
||||
DeleteSegments: true,
|
||||
Proxy: "",
|
||||
UseProxy: false,
|
||||
ConfigDir: "",
|
||||
}
|
||||
|
||||
// 1. Define command-line flags
|
||||
flag.StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (debug/info/warn/error)")
|
||||
flag.StringVar(&cfg.LogDir, "log-dir", cfg.LogDir, "Log directory")
|
||||
flag.StringVar(&cfg.DepsDir, "deps-dir", cfg.DepsDir, "Directory containing downloader tool binaries")
|
||||
flag.StringVar(&cfg.SchemaPath, "schema-path", cfg.SchemaPath, "Path to the download schema config.json")
|
||||
flag.StringVar(&cfg.Port, "port", cfg.Port, "Server port")
|
||||
flag.StringVar(&cfg.LocalDir, "local-dir", cfg.LocalDir, "Default download directory")
|
||||
flag.BoolVar(&cfg.DeleteSegments, "delete-segments", cfg.DeleteSegments, "Delete segments after download")
|
||||
flag.StringVar(&cfg.Proxy, "proxy", cfg.Proxy, "Proxy for downloader")
|
||||
flag.BoolVar(&cfg.UseProxy, "use-proxy", cfg.UseProxy, "Enable proxy")
|
||||
flag.IntVar(&cfg.MaxRunner, "max-runner", cfg.MaxRunner, "Maximum concurrent download runners")
|
||||
flag.StringVar(&cfg.DBPath, "db-path", cfg.DBPath, "Path to SQLite database file")
|
||||
flag.StringVar(&cfg.ConfigDir, "config-dir", cfg.ConfigDir, "Directory for persistent config file")
|
||||
flag.BoolVar(&cfg.EnableAuth, "enable-auth", cfg.EnableAuth, "Enable API key authentication")
|
||||
flag.StringVar(&cfg.StaticDir, "static-dir", cfg.StaticDir, "Directory to serve static files from (SPA mode)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// 3. Load from environment variables (overrides JSON and defaults)
|
||||
cfg.GinMode = getEnv("GIN_MODE", cfg.GinMode)
|
||||
cfg.Host = getEnv("HOST", cfg.Host)
|
||||
cfg.Port = getEnv("PORT", cfg.Port)
|
||||
cfg.DBPath = getEnv("DB_PATH", cfg.DBPath)
|
||||
|
||||
// If SchemaPath is still empty, compute its default value
|
||||
if cfg.SchemaPath == "" {
|
||||
cfg.SchemaPath = getDefaultSchemaPath()
|
||||
}
|
||||
|
||||
// If ConfigDir is empty, default to the LogDir path
|
||||
if cfg.ConfigDir == "" {
|
||||
cfg.ConfigDir = cfg.LogDir
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// getDefaultSchemaPath returns the default path for the schema config file.
|
||||
func getDefaultSchemaPath() string {
|
||||
// Default path: prefer config.json in the same directory as the executable
|
||||
execPath, _ := os.Executable()
|
||||
execDir := filepath.Dir(execPath)
|
||||
localConfig := filepath.Join(execDir, "config.json")
|
||||
if _, err := os.Stat(localConfig); err == nil {
|
||||
return localConfig
|
||||
}
|
||||
// Fall back to the config file path inside the repository
|
||||
return "configs/config.json"
|
||||
}
|
||||
|
||||
// exeExt returns ".exe" on Windows, empty string otherwise.
|
||||
func exeExt() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getBinaryMap builds the downloader binary path map from a single deps directory.
|
||||
func getBinaryMap(cfg *AppConfig) map[core.DownloadType]string {
|
||||
ext := exeExt()
|
||||
m := make(map[core.DownloadType]string, len(core.BinaryNames))
|
||||
for dt, name := range core.BinaryNames {
|
||||
m[dt] = filepath.Join(cfg.DepsDir, name+ext)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// getFFmpegBin returns the ffmpeg binary path derived from the deps directory.
|
||||
func getFFmpegBin(cfg *AppConfig) string {
|
||||
return filepath.Join(cfg.DepsDir, core.FFmpegBinaryName+exeExt())
|
||||
}
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// toInt converts a JSON-decoded number (float64) or int to int.
|
||||
func toInt(v any) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n), true
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
return server.Run(addr)
|
||||
}
|
||||
|
||||
+4
-1
@@ -9,10 +9,13 @@ require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.9
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/text v0.30.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.31.1
|
||||
@@ -43,6 +46,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -61,7 +65,6 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
|
||||
@@ -10,6 +10,7 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -20,6 +21,7 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
@@ -72,6 +74,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -106,6 +110,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package app
|
||||
|
||||
// AppStore holds all user-facing configuration options.
|
||||
// Field names and defaults match the TypeScript AppStore interface
|
||||
// in @mediago/shared-common.
|
||||
type AppStore struct {
|
||||
Local string `json:"local"`
|
||||
PromptTone bool `json:"promptTone"`
|
||||
Proxy string `json:"proxy"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
DeleteSegments bool `json:"deleteSegments"`
|
||||
OpenInNewWindow bool `json:"openInNewWindow"`
|
||||
BlockAds bool `json:"blockAds"`
|
||||
Theme string `json:"theme"`
|
||||
UseExtension bool `json:"useExtension"`
|
||||
IsMobile bool `json:"isMobile"`
|
||||
MaxRunner int `json:"maxRunner"`
|
||||
Language string `json:"language"`
|
||||
ShowTerminal bool `json:"showTerminal"`
|
||||
Privacy bool `json:"privacy"`
|
||||
MachineId string `json:"machineId"`
|
||||
DownloadProxySwitch bool `json:"downloadProxySwitch"`
|
||||
AutoUpgrade bool `json:"autoUpgrade"`
|
||||
AllowBeta bool `json:"allowBeta"`
|
||||
CloseMainWindow bool `json:"closeMainWindow"`
|
||||
AudioMuted bool `json:"audioMuted"`
|
||||
EnableDocker bool `json:"enableDocker"`
|
||||
DockerUrl string `json:"dockerUrl"`
|
||||
EnableMobilePlayer bool `json:"enableMobilePlayer"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
}
|
||||
|
||||
// DefaultAppStore returns default config values matching the TS appStoreDefaults.
|
||||
func DefaultAppStore() AppStore {
|
||||
return AppStore{
|
||||
Local: "",
|
||||
PromptTone: true,
|
||||
Proxy: "",
|
||||
UseProxy: false,
|
||||
DeleteSegments: true,
|
||||
OpenInNewWindow: false,
|
||||
BlockAds: true,
|
||||
Theme: "system",
|
||||
UseExtension: false,
|
||||
IsMobile: false,
|
||||
MaxRunner: 2,
|
||||
Language: "system",
|
||||
ShowTerminal: false,
|
||||
Privacy: false,
|
||||
MachineId: "",
|
||||
DownloadProxySwitch: false,
|
||||
AutoUpgrade: true,
|
||||
AllowBeta: false,
|
||||
CloseMainWindow: false,
|
||||
AudioMuted: true,
|
||||
EnableDocker: false,
|
||||
DockerUrl: "",
|
||||
EnableMobilePlayer: false,
|
||||
ApiKey: "",
|
||||
PasswordHash: "",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// AppConfig stores startup configuration options passed by flags or environment.
|
||||
type AppConfig struct {
|
||||
GinMode string `json:"gin_mode"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
LogLevel string `json:"log_level"`
|
||||
LogDir string `json:"log_dir"`
|
||||
SchemaPath string `json:"schema_path"`
|
||||
DepsDir string `json:"deps_dir"`
|
||||
MaxRunner int `json:"max_runner"`
|
||||
LocalDir string `json:"local_dir"`
|
||||
DeleteSegments bool `json:"delete_segments"`
|
||||
Proxy string `json:"proxy"`
|
||||
UseProxy bool `json:"use_proxy"`
|
||||
DBPath string `json:"db_path"`
|
||||
ConfigDir string `json:"config_dir"`
|
||||
EnableAuth bool `json:"enable_auth"`
|
||||
StaticDir string `json:"static_dir"`
|
||||
}
|
||||
|
||||
func DefaultConfig() *AppConfig {
|
||||
return &AppConfig{
|
||||
GinMode: "release",
|
||||
Host: "0.0.0.0",
|
||||
Port: "8080",
|
||||
LogLevel: "info",
|
||||
LogDir: "./logs",
|
||||
SchemaPath: "",
|
||||
DepsDir: "",
|
||||
MaxRunner: 2,
|
||||
LocalDir: "./downloads",
|
||||
DeleteSegments: true,
|
||||
Proxy: "",
|
||||
UseProxy: false,
|
||||
ConfigDir: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AppConfig) ApplyEnvAndDefaults() {
|
||||
c.GinMode = getEnv("GIN_MODE", c.GinMode)
|
||||
c.Host = getEnv("HOST", c.Host)
|
||||
c.Port = getEnv("PORT", c.Port)
|
||||
c.DBPath = getEnv("DB_PATH", c.DBPath)
|
||||
|
||||
if c.SchemaPath == "" {
|
||||
c.SchemaPath = getDefaultSchemaPath()
|
||||
}
|
||||
if c.ConfigDir == "" {
|
||||
c.ConfigDir = c.LogDir
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetLocalDir() string {
|
||||
return c.LocalDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetDeleteSegments() bool {
|
||||
return c.DeleteSegments
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetProxy() string {
|
||||
return c.Proxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUseProxy() bool {
|
||||
return c.UseProxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetLocalDir(dir string) {
|
||||
c.LocalDir = dir
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetDeleteSegments(del bool) {
|
||||
c.DeleteSegments = del
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetProxy(proxy string) {
|
||||
c.Proxy = proxy
|
||||
}
|
||||
|
||||
func (c *AppConfig) SetUseProxy(useProxy bool) {
|
||||
c.UseProxy = useProxy
|
||||
}
|
||||
|
||||
func getDefaultSchemaPath() string {
|
||||
execPath, _ := os.Executable()
|
||||
execDir := filepath.Dir(execPath)
|
||||
localConfig := filepath.Join(execDir, "config.json")
|
||||
if _, err := os.Stat(localConfig); err == nil {
|
||||
return localConfig
|
||||
}
|
||||
return "configs/config.json"
|
||||
}
|
||||
|
||||
func getEnv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"caorushizi.cn/mediago/internal/core"
|
||||
"caorushizi.cn/mediago/internal/core/runner"
|
||||
"caorushizi.cn/mediago/internal/core/schema"
|
||||
"caorushizi.cn/mediago/internal/db"
|
||||
"caorushizi.cn/mediago/internal/logger"
|
||||
"caorushizi.cn/mediago/internal/tasklog"
|
||||
"caorushizi.cn/mediago/pkg/conf"
|
||||
)
|
||||
|
||||
type Runtime struct {
|
||||
Config *AppConfig
|
||||
AppStore *conf.Conf[AppStore]
|
||||
Downloader *core.DownloaderSvc
|
||||
Queue *core.TaskQueue
|
||||
TaskLogs *tasklog.Manager
|
||||
Database *db.Database
|
||||
}
|
||||
|
||||
func InitLogger(cfg *AppConfig) error {
|
||||
logCfg := logger.DefaultConfig()
|
||||
logCfg.Level = cfg.LogLevel
|
||||
logCfg.LogDir = cfg.LogDir
|
||||
|
||||
if err := logger.Init(logCfg); err != nil {
|
||||
return fmt.Errorf("failed to initialize logger: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRuntime(cfg *AppConfig) (*Runtime, error) {
|
||||
logger.Info("MediaGo Downloader Core Starting...")
|
||||
logger.Infof("Final Config: %+v", cfg)
|
||||
|
||||
appStore, err := conf.New(conf.Options[AppStore]{
|
||||
ConfigName: "config",
|
||||
CWD: cfg.ConfigDir,
|
||||
Defaults: DefaultAppStore(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize app store: %w", err)
|
||||
}
|
||||
logger.Infof("App store initialized at: %s", appStore.Path())
|
||||
|
||||
if appStore.Store().MachineId == "" {
|
||||
newId := uuid.New().String()
|
||||
_ = appStore.Set("machineId", newId)
|
||||
logger.Infof("Generated new machineId: %s", newId)
|
||||
}
|
||||
|
||||
syncAppStoreToCfg(appStore, cfg)
|
||||
ensureDownloadDir(appStore, cfg)
|
||||
|
||||
logger.Infof("Loading schemas from: %s", cfg.SchemaPath)
|
||||
schemas, err := schema.LoadSchemasFromJSON(cfg.SchemaPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load schemas: %w", err)
|
||||
}
|
||||
logger.Infof("Loaded %d download schemas", len(schemas.Schemas))
|
||||
|
||||
binMap := getBinaryMap(cfg)
|
||||
for dt, binPath := range binMap {
|
||||
logger.Infof("%s downloader: %s", dt, binPath)
|
||||
if binPath == "" {
|
||||
continue
|
||||
}
|
||||
if info, err := os.Stat(binPath); err != nil {
|
||||
logger.Warnf("%s binary not found: %v", dt, err)
|
||||
} else if info.Mode()&0o111 == 0 {
|
||||
logger.Warnf("%s binary is not executable: %s", dt, binPath)
|
||||
}
|
||||
}
|
||||
|
||||
r := runner.NewPTYRunner()
|
||||
downloader := core.NewDownloader(binMap, r, schemas, cfg)
|
||||
queue := core.NewTaskQueue(downloader, cfg.MaxRunner)
|
||||
taskLogs := tasklog.NewManager(filepath.Join(cfg.LogDir, "tasks"))
|
||||
|
||||
logger.Infof("Task queue initialized (maxRunner=%d)", cfg.MaxRunner)
|
||||
logger.Infof("Task logs will be stored in %s", filepath.Join(cfg.LogDir, "tasks"))
|
||||
|
||||
appStore.OnDidChange("maxRunner", func(newVal, oldVal any) {
|
||||
if v, ok := toInt(newVal); ok {
|
||||
queue.SetMaxRunner(v)
|
||||
logger.Infof("maxRunner updated to %d via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("proxy", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(string); ok {
|
||||
cfg.SetProxy(v)
|
||||
logger.Infof("proxy updated to %q via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("useProxy", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(bool); ok {
|
||||
cfg.SetUseProxy(v)
|
||||
logger.Infof("useProxy updated to %v via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("deleteSegments", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(bool); ok {
|
||||
cfg.SetDeleteSegments(v)
|
||||
logger.Infof("deleteSegments updated to %v via config change", v)
|
||||
}
|
||||
})
|
||||
appStore.OnDidChange("local", func(newVal, oldVal any) {
|
||||
if v, ok := newVal.(string); ok {
|
||||
cfg.SetLocalDir(v)
|
||||
logger.Infof("localDir updated to %q via config change", v)
|
||||
}
|
||||
})
|
||||
|
||||
var database *db.Database
|
||||
if cfg.DBPath != "" {
|
||||
if dir := filepath.Dir(cfg.DBPath); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
var dbErr error
|
||||
database, dbErr = db.New(cfg.DBPath)
|
||||
if dbErr != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", dbErr)
|
||||
}
|
||||
logger.Infof("Database opened: %s", cfg.DBPath)
|
||||
} else {
|
||||
logger.Info("No database path provided, running without persistence")
|
||||
}
|
||||
|
||||
return &Runtime{
|
||||
Config: cfg,
|
||||
AppStore: appStore,
|
||||
Downloader: downloader,
|
||||
Queue: queue,
|
||||
TaskLogs: taskLogs,
|
||||
Database: database,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rt *Runtime) Close() {
|
||||
if rt.Database != nil {
|
||||
_ = rt.Database.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func syncAppStoreToCfg(store *conf.Conf[AppStore], cfg *AppConfig) {
|
||||
s := store.Store()
|
||||
cliLocalDir := cfg.LocalDir
|
||||
cliExplicit := cliLocalDir != "" && cliLocalDir != "./downloads"
|
||||
|
||||
if cliExplicit {
|
||||
_ = store.Set("local", cliLocalDir)
|
||||
} else if s.Local != "" {
|
||||
cfg.LocalDir = s.Local
|
||||
}
|
||||
cfg.Proxy = s.Proxy
|
||||
cfg.UseProxy = s.UseProxy
|
||||
cfg.DeleteSegments = s.DeleteSegments
|
||||
if s.MaxRunner > 0 {
|
||||
cfg.MaxRunner = s.MaxRunner
|
||||
}
|
||||
}
|
||||
|
||||
func ensureDownloadDir(store *conf.Conf[AppStore], cfg *AppConfig) {
|
||||
needDefault := cfg.LocalDir == "" || cfg.LocalDir == "./downloads"
|
||||
if !needDefault {
|
||||
if info, err := os.Stat(cfg.LocalDir); err != nil || !info.IsDir() {
|
||||
needDefault = true
|
||||
}
|
||||
}
|
||||
if needDefault {
|
||||
sysDownloads := getSystemDownloadsDir()
|
||||
logger.Infof("Download dir %q unavailable, using system default: %s", cfg.LocalDir, sysDownloads)
|
||||
cfg.LocalDir = sysDownloads
|
||||
}
|
||||
if store.Store().Local == "" {
|
||||
_ = store.Set("local", cfg.LocalDir)
|
||||
}
|
||||
}
|
||||
|
||||
func getSystemDownloadsDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
downloads := filepath.Join(home, "Downloads")
|
||||
if info, err := os.Stat(downloads); err == nil && info.IsDir() {
|
||||
return downloads
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
func exeExt() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getBinaryMap(cfg *AppConfig) map[core.DownloadType]string {
|
||||
ext := exeExt()
|
||||
m := make(map[core.DownloadType]string, len(core.BinaryNames))
|
||||
for dt, name := range core.BinaryNames {
|
||||
m[dt] = filepath.Join(cfg.DepsDir, name+ext)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func FFmpegBinPath(cfg *AppConfig) string {
|
||||
return filepath.Join(cfg.DepsDir, core.FFmpegBinaryName+exeExt())
|
||||
}
|
||||
|
||||
func toInt(v any) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n), true
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user