Files
ncore-hnr/internal/config/config.go
Zsolt Alföldi ecea084003 Add support for notification dry run feature
- Introduced NOTIFICATION_DRY_RUN configuration option in .env.example and k8s/secret.example.yaml.
- Updated README.md to include usage instructions for the new dry run feature.
- Implemented logic in app.go to preview notifications without sending them when the dry run option is enabled.
- Enhanced config.go to load the new configuration option and validate notification types accordingly.
- Added a new function in notify.go to generate manual-needed notification messages for preview.
2026-05-07 00:35:05 +02:00

229 lines
7.7 KiB
Go

package config
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
type Config struct {
NCoreUsername string
NCorePassword string
NCoreLoginURL string
NCoreHitRunURL string
QBitURL string
QBitUsername string
QBitPassword string
SkipQBit bool
DBPath string
DryRun bool
JSONOutput bool
AlertAfter time.Duration
HTTPTimeout time.Duration
NotificationDryRun bool
NotificationType string
NotificationNTFYURL string
NotificationSMTPHost string
NotificationSMTPPort string
NotificationSMTPUsername string
NotificationSMTPPassword string
NotificationSMTPFrom string
NotificationSMTPTo string
}
func Load(args []string) (Config, error) {
loadDotenv(".env", "../.env")
cfg := Config{
NCoreUsername: env("NCORE_USERNAME", ""),
NCorePassword: env("NCORE_PASSWORD", ""),
NCoreLoginURL: env("NCORE_LOGIN_URL", "https://ncore.pro/login.php"),
NCoreHitRunURL: env("NCORE_HITRUN_URL", "https://ncore.pro/hitnrun.php"),
QBitURL: env("QBITTORRENT_URL", ""),
QBitUsername: env("QBITTORRENT_USERNAME", ""),
QBitPassword: env("QBITTORRENT_PASSWORD", ""),
SkipQBit: envBool("SKIP_QBIT", false),
DBPath: env("APP_DB_PATH", "data/ncore-hnr.sqlite"),
DryRun: envBool("DRY_RUN", false),
JSONOutput: envBool("JSON_OUTPUT", false),
AlertAfter: envDuration("ALERT_AFTER", 48*time.Hour),
HTTPTimeout: envDuration("HTTP_TIMEOUT", 30*time.Second),
NotificationDryRun: envBool("NOTIFICATION_DRY_RUN", false),
NotificationType: env("NOTIFICATION_TYPE", ""),
NotificationNTFYURL: env("NOTIFICATION_NTFY_URL", env("NOTIFY_URL", "")),
NotificationSMTPHost: env("NOTIFICATION_SMTP_HOST", ""),
NotificationSMTPPort: env("NOTIFICATION_SMTP_PORT", "587"),
NotificationSMTPUsername: env("NOTIFICATION_SMTP_USERNAME", ""),
NotificationSMTPPassword: env("NOTIFICATION_SMTP_PASSWORD", ""),
NotificationSMTPFrom: env("NOTIFICATION_SMTP_FROM", ""),
NotificationSMTPTo: env("NOTIFICATION_SMTP_TO", ""),
}
fs := flag.NewFlagSet("ncore-hnr", flag.ContinueOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "print intended qBittorrent actions without executing them")
fs.BoolVar(&cfg.SkipQBit, "skip-qbit", cfg.SkipQBit, "skip qBittorrent matching/actions")
fs.BoolVar(&cfg.JSONOutput, "json", cfg.JSONOutput, "print JSON summary")
fs.DurationVar(&cfg.AlertAfter, "alert-after", cfg.AlertAfter, "mark active torrents manual-needed after this duration")
fs.BoolVar(&cfg.NotificationDryRun, "notification-dry-run", cfg.NotificationDryRun, "print the manual-needed notification without sending it")
fs.StringVar(&cfg.NotificationType, "notification-type", cfg.NotificationType, "notification sender: ntfy, smtp, or empty")
fs.StringVar(&cfg.NotificationNTFYURL, "notification-ntfy-url", cfg.NotificationNTFYURL, "ntfy topic URL for manual-needed alerts")
fs.StringVar(&cfg.NotificationNTFYURL, "notify-url", cfg.NotificationNTFYURL, "deprecated: use --notification-ntfy-url")
fs.StringVar(&cfg.NotificationSMTPHost, "notification-smtp-host", cfg.NotificationSMTPHost, "SMTP host for manual-needed alerts")
fs.StringVar(&cfg.NotificationSMTPPort, "notification-smtp-port", cfg.NotificationSMTPPort, "SMTP port for manual-needed alerts")
fs.StringVar(&cfg.NotificationSMTPUsername, "notification-smtp-username", cfg.NotificationSMTPUsername, "SMTP username for manual-needed alerts")
fs.StringVar(&cfg.NotificationSMTPPassword, "notification-smtp-password", cfg.NotificationSMTPPassword, "SMTP password for manual-needed alerts")
fs.StringVar(&cfg.NotificationSMTPFrom, "notification-smtp-from", cfg.NotificationSMTPFrom, "SMTP From header for manual-needed alerts")
fs.StringVar(&cfg.NotificationSMTPTo, "notification-smtp-to", cfg.NotificationSMTPTo, "SMTP To header for manual-needed alerts")
fs.DurationVar(&cfg.HTTPTimeout, "http-timeout", cfg.HTTPTimeout, "HTTP timeout")
if err := fs.Parse(args); err != nil {
return cfg, err
}
cfg.NotificationType = normalizeNotificationType(cfg)
if cfg.NCoreUsername == "" {
return cfg, fmt.Errorf("missing NCORE_USERNAME")
}
if cfg.NCorePassword == "" {
return cfg, fmt.Errorf("missing NCORE_PASSWORD")
}
if !cfg.SkipQBit && !cfg.DryRun {
if cfg.QBitURL == "" || cfg.QBitUsername == "" || cfg.QBitPassword == "" {
return cfg, fmt.Errorf("set QBITTORRENT_URL, QBITTORRENT_USERNAME and QBITTORRENT_PASSWORD, or use --dry-run/--skip-qbit")
}
}
if !cfg.NotificationDryRun {
if err := validateNotification(cfg); err != nil {
return cfg, err
}
} else if err := validateNotificationType(cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func LoadReadOnly(args []string) (Config, error) {
loadDotenv(".env", "../.env")
cfg := Config{
DBPath: env("APP_DB_PATH", "data/ncore-hnr.sqlite"),
JSONOutput: envBool("JSON_OUTPUT", false),
}
fs := flag.NewFlagSet("ncore-hnr", flag.ContinueOnError)
fs.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
fs.BoolVar(&cfg.JSONOutput, "json", cfg.JSONOutput, "print JSON output")
if err := fs.Parse(args); err != nil {
return cfg, err
}
return cfg, nil
}
func loadDotenv(paths ...string) {
for _, path := range paths {
_ = godotenv.Load(path)
}
}
func env(name string, fallback string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
return value
}
func envBool(name string, fallback bool) bool {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return fallback
}
return parsed
}
func envDuration(name string, fallback time.Duration) time.Duration {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil {
return fallback
}
return parsed
}
func normalizeNotificationType(cfg Config) string {
notificationType := strings.ToLower(strings.TrimSpace(cfg.NotificationType))
switch notificationType {
case "none", "off", "disabled":
return ""
case "":
if strings.TrimSpace(cfg.NotificationNTFYURL) != "" {
return "ntfy"
}
if strings.TrimSpace(cfg.NotificationSMTPHost) != "" || strings.TrimSpace(cfg.NotificationSMTPTo) != "" {
return "smtp"
}
}
return notificationType
}
func validateNotification(cfg Config) error {
if err := validateNotificationType(cfg); err != nil {
return err
}
switch cfg.NotificationType {
case "":
return nil
case "ntfy":
if strings.TrimSpace(cfg.NotificationNTFYURL) == "" {
return fmt.Errorf("missing NOTIFICATION_NTFY_URL for ntfy notifications")
}
case "smtp":
if strings.TrimSpace(cfg.NotificationSMTPHost) == "" {
return fmt.Errorf("missing NOTIFICATION_SMTP_HOST for smtp notifications")
}
if strings.TrimSpace(cfg.NotificationSMTPPort) == "" {
return fmt.Errorf("missing NOTIFICATION_SMTP_PORT for smtp notifications")
}
if strings.TrimSpace(cfg.NotificationSMTPFrom) == "" {
return fmt.Errorf("missing NOTIFICATION_SMTP_FROM for smtp notifications")
}
if strings.TrimSpace(cfg.NotificationSMTPTo) == "" {
return fmt.Errorf("missing NOTIFICATION_SMTP_TO for smtp notifications")
}
if (strings.TrimSpace(cfg.NotificationSMTPUsername) == "") != (strings.TrimSpace(cfg.NotificationSMTPPassword) == "") {
return fmt.Errorf("set both NOTIFICATION_SMTP_USERNAME and NOTIFICATION_SMTP_PASSWORD, or leave both empty")
}
default:
return fmt.Errorf("unsupported NOTIFICATION_TYPE %q", cfg.NotificationType)
}
return nil
}
func validateNotificationType(cfg Config) error {
switch cfg.NotificationType {
case "", "ntfy", "smtp":
return nil
default:
return fmt.Errorf("unsupported NOTIFICATION_TYPE %q", cfg.NotificationType)
}
}