feat: implement webhook notification system and integrate into queue manager
Build / Build and Push (push) Successful in 51s
Build / Build and Push (push) Successful in 51s
This commit is contained in:
@@ -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.
|
- **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.
|
- **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.
|
- **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.
|
- **Docker-Ready**: Packaged in an ultra-slim container image based on Debian, with `ffmpeg` built-in.
|
||||||
|
|
||||||
## Deployment with Docker
|
## Deployment with Docker
|
||||||
@@ -52,6 +53,9 @@ services:
|
|||||||
- GOENCODE_AUTH_USER=admin
|
- GOENCODE_AUTH_USER=admin
|
||||||
- GOENCODE_AUTH_PASS=secret
|
- GOENCODE_AUTH_PASS=secret
|
||||||
|
|
||||||
|
# Notifications (Optional)
|
||||||
|
- GOENCODE_WEBHOOK_URL=http://your-webhook-endpoint.com/webhook
|
||||||
|
|
||||||
# Encoding
|
# Encoding
|
||||||
- GOENCODE_ENCODER_TEMP=/tmp/goencode
|
- GOENCODE_ENCODER_TEMP=/tmp/goencode
|
||||||
volumes:
|
volumes:
|
||||||
@@ -93,6 +97,7 @@ GoEncode can be configured via `goencode.yaml` or entirely via environment varia
|
|||||||
| `TZ` | Container TimeZone | `UTC` |
|
| `TZ` | Container TimeZone | `UTC` |
|
||||||
| `GOENCODE_AUTH_USER` | Username for the web UI | |
|
| `GOENCODE_AUTH_USER` | Username for the web UI | |
|
||||||
| `GOENCODE_AUTH_PASS` | Password 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` |
|
| `GOENCODE_ENCODER_TEMP`| Temp directory for processing jobs | `/tmp/goencode` |
|
||||||
|
|
||||||
## Building Locally
|
## Building Locally
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func main() {
|
|||||||
// Initialize logger
|
// Initialize logger
|
||||||
logger.Init(sseServer.Broadcast)
|
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()
|
qm.Start()
|
||||||
defer qm.Stop()
|
defer qm.Stop()
|
||||||
|
|
||||||
|
|||||||
Submodule
+1
Submodule godump added at 9a417155fc
@@ -52,12 +52,17 @@ type AuthConfig struct {
|
|||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationsConfig struct {
|
||||||
|
WebhookURL string `yaml:"webhook_url"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Auth AuthConfig `yaml:"auth"`
|
Auth AuthConfig `yaml:"auth"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
Encoder EncoderConfig `yaml:"encoder"`
|
Encoder EncoderConfig `yaml:"encoder"`
|
||||||
Logging LoggingConfig `yaml:"logging"`
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
Notifications NotificationsConfig `yaml:"notifications"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
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.FFmpegPath = getEnvStr("GOENCODE_FFMPEG_PATH", cfg.Encoder.FFmpegPath)
|
||||||
cfg.Encoder.TempDir = getEnvStr("GOENCODE_ENCODER_TEMP", cfg.Encoder.TempDir)
|
cfg.Encoder.TempDir = getEnvStr("GOENCODE_ENCODER_TEMP", cfg.Encoder.TempDir)
|
||||||
cfg.Logging.Level = getEnvStr("GOENCODE_LOG_LEVEL", cfg.Logging.Level)
|
cfg.Logging.Level = getEnvStr("GOENCODE_LOG_LEVEL", cfg.Logging.Level)
|
||||||
|
cfg.Notifications.WebhookURL = getEnvStr("GOENCODE_WEBHOOK_URL", cfg.Notifications.WebhookURL)
|
||||||
|
|
||||||
// Defaults if missing entirely
|
// Defaults if missing entirely
|
||||||
if cfg.Server.Port == 0 {
|
if cfg.Server.Port == 0 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -14,18 +14,20 @@ type Manager struct {
|
|||||||
TriggerChan chan struct{}
|
TriggerChan chan struct{}
|
||||||
StopChan chan struct{}
|
StopChan chan struct{}
|
||||||
Broadcast func(string, interface{})
|
Broadcast func(string, interface{})
|
||||||
|
WebhookURL string
|
||||||
encoder *encoder.FFmpegManager
|
encoder *encoder.FFmpegManager
|
||||||
isProcessing bool
|
isProcessing bool
|
||||||
mu sync.Mutex
|
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{
|
return &Manager{
|
||||||
FFmpegPath: ffmpegPath,
|
FFmpegPath: ffmpegPath,
|
||||||
TempDir: tempDir,
|
TempDir: tempDir,
|
||||||
TriggerChan: make(chan struct{}, 1),
|
TriggerChan: make(chan struct{}, 1),
|
||||||
StopChan: make(chan struct{}),
|
StopChan: make(chan struct{}),
|
||||||
Broadcast: broadcast,
|
Broadcast: broadcast,
|
||||||
|
WebhookURL: webhookURL,
|
||||||
encoder: encoder.NewManager(ffmpegPath),
|
encoder: encoder.NewManager(ffmpegPath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"goencode/internal/db"
|
"goencode/internal/db"
|
||||||
"goencode/internal/encoder"
|
"goencode/internal/encoder"
|
||||||
|
"goencode/internal/notify"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +99,9 @@ func (m *Manager) processNextJob() {
|
|||||||
db.AddJobReport(job, "failed", 0, 0, 0)
|
db.AddJobReport(job, "failed", 0, 0, 0)
|
||||||
db.DeleteJob(job.ID)
|
db.DeleteJob(job.ID)
|
||||||
m.NotifySSE("job_failed", map[string]interface{}{"id": job.ID, "error": err.Error()})
|
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 {
|
} else {
|
||||||
log.Printf("Job %d succeeded", job.ID)
|
log.Printf("Job %d succeeded", job.ID)
|
||||||
db.DeleteJob(job.ID)
|
db.DeleteJob(job.ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user