diff --git a/README.md b/README.md index 3ea792b..daebd3a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ GoEncode is a lightweight, high-performance media transcoding server written in - **Format Intelligence**: Probes media to detect codecs and resolution, automatically skipping files that already meet the target codec/resolution. - **Web Dashboard**: Modern, responsive interface with Server-Sent Events (SSE) for live tracking of job progress, queue stats, and server logs. - **Robust Persistence**: Job history, metrics (size saved, time taken), and configuration are all saved to a MariaDB/MySQL database. +- **Webhook Notifications**: Optional integration to send a JSON webhook payload when an encoding job fails. - **Docker-Ready**: Packaged in an ultra-slim container image based on Debian, with `ffmpeg` built-in. ## Deployment with Docker @@ -52,6 +53,9 @@ services: - GOENCODE_AUTH_USER=admin - GOENCODE_AUTH_PASS=secret + # Notifications (Optional) + - GOENCODE_WEBHOOK_URL=http://your-webhook-endpoint.com/webhook + # Encoding - GOENCODE_ENCODER_TEMP=/tmp/goencode volumes: @@ -93,6 +97,7 @@ GoEncode can be configured via `goencode.yaml` or entirely via environment varia | `TZ` | Container TimeZone | `UTC` | | `GOENCODE_AUTH_USER` | Username for the web UI | | | `GOENCODE_AUTH_PASS` | Password for the web UI | | +| `GOENCODE_WEBHOOK_URL` | Webhook URL for job failure notifications | | | `GOENCODE_ENCODER_TEMP`| Temp directory for processing jobs | `/tmp/goencode` | ## Building Locally diff --git a/cmd/goencode/main.go b/cmd/goencode/main.go index f6cf733..867859b 100644 --- a/cmd/goencode/main.go +++ b/cmd/goencode/main.go @@ -33,7 +33,7 @@ func main() { // Initialize logger logger.Init(sseServer.Broadcast) - qm := queue.NewManager(cfg.Encoder.FFmpegPath, cfg.Encoder.TempDir, sseServer.Broadcast) + qm := queue.NewManager(cfg.Encoder.FFmpegPath, cfg.Encoder.TempDir, cfg.Notifications.WebhookURL, sseServer.Broadcast) qm.Start() defer qm.Stop() diff --git a/godump b/godump new file mode 160000 index 0000000..9a41715 --- /dev/null +++ b/godump @@ -0,0 +1 @@ +Subproject commit 9a417155fcbcc106011d2efc70dfae8ff804ae1e diff --git a/internal/config/config.go b/internal/config/config.go index d7febbf..5574d62 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,12 +52,17 @@ type AuthConfig struct { Password string `yaml:"password"` } +type NotificationsConfig struct { + WebhookURL string `yaml:"webhook_url"` +} + type Config struct { Server ServerConfig `yaml:"server"` Auth AuthConfig `yaml:"auth"` Database DatabaseConfig `yaml:"database"` - Encoder EncoderConfig `yaml:"encoder"` - Logging LoggingConfig `yaml:"logging"` + Encoder EncoderConfig `yaml:"encoder"` + Logging LoggingConfig `yaml:"logging"` + Notifications NotificationsConfig `yaml:"notifications"` } func LoadConfig(path string) (*Config, error) { @@ -84,6 +89,7 @@ func LoadConfig(path string) (*Config, error) { cfg.Encoder.FFmpegPath = getEnvStr("GOENCODE_FFMPEG_PATH", cfg.Encoder.FFmpegPath) cfg.Encoder.TempDir = getEnvStr("GOENCODE_ENCODER_TEMP", cfg.Encoder.TempDir) cfg.Logging.Level = getEnvStr("GOENCODE_LOG_LEVEL", cfg.Logging.Level) + cfg.Notifications.WebhookURL = getEnvStr("GOENCODE_WEBHOOK_URL", cfg.Notifications.WebhookURL) // Defaults if missing entirely if cfg.Server.Port == 0 { diff --git a/internal/notify/notify.go b/internal/notify/notify.go new file mode 100644 index 0000000..91ac92c --- /dev/null +++ b/internal/notify/notify.go @@ -0,0 +1,54 @@ +package notify + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "time" +) + +type WebhookPayload struct { + Title string `json:"title"` + Message string `json:"message"` + Time string `json:"time"` +} + +func SendWebhook(url, title, message string) { + if url == "" { + return + } + + payload := WebhookPayload{ + Title: title, + Message: message, + Time: time.Now().Format(time.RFC3339), + } + + data, err := json.Marshal(payload) + if err != nil { + log.Printf("Failed to marshal webhook payload: %v", err) + return + } + + go func() { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + log.Printf("Failed to create webhook request: %v", err) + return + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to send webhook: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + log.Printf("Webhook returned error status: %d", resp.StatusCode) + } + }() +} diff --git a/internal/queue/manager.go b/internal/queue/manager.go index a570ba6..db8a200 100644 --- a/internal/queue/manager.go +++ b/internal/queue/manager.go @@ -14,18 +14,20 @@ type Manager struct { TriggerChan chan struct{} StopChan chan struct{} Broadcast func(string, interface{}) + WebhookURL string encoder *encoder.FFmpegManager isProcessing bool mu sync.Mutex } -func NewManager(ffmpegPath, tempDir string, broadcast func(string, interface{})) *Manager { +func NewManager(ffmpegPath, tempDir, webhookURL string, broadcast func(string, interface{})) *Manager { return &Manager{ FFmpegPath: ffmpegPath, TempDir: tempDir, TriggerChan: make(chan struct{}, 1), StopChan: make(chan struct{}), Broadcast: broadcast, + WebhookURL: webhookURL, encoder: encoder.NewManager(ffmpegPath), } } diff --git a/internal/queue/worker.go b/internal/queue/worker.go index 39c7b99..a96fc89 100644 --- a/internal/queue/worker.go +++ b/internal/queue/worker.go @@ -13,6 +13,7 @@ import ( "goencode/internal/db" "goencode/internal/encoder" + "goencode/internal/notify" "io" ) @@ -98,6 +99,9 @@ func (m *Manager) processNextJob() { db.AddJobReport(job, "failed", 0, 0, 0) db.DeleteJob(job.ID) m.NotifySSE("job_failed", map[string]interface{}{"id": job.ID, "error": err.Error()}) + if m.WebhookURL != "" { + notify.SendWebhook(m.WebhookURL, "Encoding Job Failed", fmt.Sprintf("File: %s\nError: %s", job.FilePath, err.Error())) + } } else { log.Printf("Job %d succeeded", job.ID) db.DeleteJob(job.ID)