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.
This commit is contained in:
@@ -122,6 +122,11 @@ func Run(cfg config.Config) (model.RunSummary, error) {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
if cfg.NotificationDryRun {
|
||||
printNotificationPreview(cfg.NotificationType, summary.Results)
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
notifier := newNotifier(cfg)
|
||||
if notifier != nil {
|
||||
if err := notifier.SendManualNeeded(summary.Results); err != nil {
|
||||
@@ -300,3 +305,17 @@ func newNotifier(cfg config.Config) notify.Sender {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func printNotificationPreview(notificationType string, results []model.ActionResult) {
|
||||
subject, body, ok := notify.ManualNeededMessage(results)
|
||||
if !ok {
|
||||
fmt.Fprintln(os.Stderr, "notification dry-run: no manual-needed torrents, nothing would be sent")
|
||||
return
|
||||
}
|
||||
|
||||
if notificationType == "" {
|
||||
notificationType = "unconfigured"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "notification dry-run: type=%s\n", notificationType)
|
||||
fmt.Fprintf(os.Stderr, "Subject: %s\n\n%s", subject, body)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ type Config struct {
|
||||
AlertAfter time.Duration
|
||||
HTTPTimeout time.Duration
|
||||
|
||||
NotificationDryRun bool
|
||||
NotificationType string
|
||||
NotificationNTFYURL string
|
||||
NotificationSMTPHost string
|
||||
@@ -56,6 +57,7 @@ func Load(args []string) (Config, error) {
|
||||
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", ""),
|
||||
@@ -72,6 +74,7 @@ func Load(args []string) (Config, error) {
|
||||
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")
|
||||
@@ -99,7 +102,11 @@ func Load(args []string) (Config, error) {
|
||||
return cfg, fmt.Errorf("set QBITTORRENT_URL, QBITTORRENT_USERNAME and QBITTORRENT_PASSWORD, or use --dry-run/--skip-qbit")
|
||||
}
|
||||
}
|
||||
if err := validateNotification(cfg); err != nil {
|
||||
if !cfg.NotificationDryRun {
|
||||
if err := validateNotification(cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
} else if err := validateNotificationType(cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -179,6 +186,9 @@ func normalizeNotificationType(cfg Config) string {
|
||||
}
|
||||
|
||||
func validateNotification(cfg Config) error {
|
||||
if err := validateNotificationType(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
switch cfg.NotificationType {
|
||||
case "":
|
||||
return nil
|
||||
@@ -207,3 +217,12 @@ func validateNotification(cfg Config) error {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,11 @@ func (s NotificationSMTP) SendManualNeeded(results []model.ActionResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ManualNeededMessage(results []model.ActionResult) (string, string, bool) {
|
||||
body, ok := manualNeededText(results)
|
||||
return manualNeededSubject, body, ok
|
||||
}
|
||||
|
||||
func manualNeededText(results []model.ActionResult) (string, bool) {
|
||||
var body strings.Builder
|
||||
manualCount := 0
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"ncore-hnr/internal/model"
|
||||
)
|
||||
|
||||
var releaseSeparator = regexp.MustCompile(`[._\-\[\]()+]+`)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
@@ -87,7 +90,8 @@ func MatchByName(ncoreName string, torrents []model.QBitTorrent) (model.QBitTorr
|
||||
|
||||
normalized := normalizeName(ncoreName)
|
||||
for _, torrent := range torrents {
|
||||
if normalizeName(torrent.Name) == normalized {
|
||||
torrentName := normalizeName(torrent.Name)
|
||||
if torrentName == normalized || strings.HasPrefix(torrentName, normalized+" ") {
|
||||
return torrent, true
|
||||
}
|
||||
}
|
||||
@@ -112,5 +116,6 @@ func (c *Client) endpoint(path string) string {
|
||||
}
|
||||
|
||||
func normalizeName(value string) string {
|
||||
value = releaseSeparator.ReplaceAllString(value, " ")
|
||||
return strings.ToLower(strings.Join(strings.Fields(value), " "))
|
||||
}
|
||||
|
||||
31
internal/qbit/client_test.go
Normal file
31
internal/qbit/client_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"ncore-hnr/internal/model"
|
||||
)
|
||||
|
||||
func TestMatchByNameMatchesLongerReleaseName(t *testing.T) {
|
||||
torrents := []model.QBitTorrent{
|
||||
{Name: "The.Black.Dagger.Brotherhood.S01E01.720p.WEB.h264-GROUP", Hash: "match"},
|
||||
}
|
||||
|
||||
match, ok := MatchByName("The Black Dagger Brotherhood S01E01 720p", torrents)
|
||||
if !ok {
|
||||
t.Fatal("expected qBittorrent release name to match nCore display name")
|
||||
}
|
||||
if match.Hash != "match" {
|
||||
t.Fatalf("expected match hash %q, got %q", "match", match.Hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchByNameDoesNotMatchDifferentEpisode(t *testing.T) {
|
||||
torrents := []model.QBitTorrent{
|
||||
{Name: "The.Black.Dagger.Brotherhood.S01E02.720p.WEB.h264-GROUP", Hash: "wrong"},
|
||||
}
|
||||
|
||||
if _, ok := MatchByName("The Black Dagger Brotherhood S01E01 720p", torrents); ok {
|
||||
t.Fatal("expected different episode not to match")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user