diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..2df2bfb
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(go test:*)"
+ ]
+ }
+}
diff --git a/.deploy/stream.api-production.yaml b/.deploy/stream.api-production.yaml
new file mode 100644
index 0000000..12af4c6
--- /dev/null
+++ b/.deploy/stream.api-production.yaml
@@ -0,0 +1,89 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: stream-api-config
+ namespace: stream-production
+ labels:
+ app: stream-api
+data:
+ config.yaml: |
+ server:
+ grpc_port: "9000"
+ mode: "release"
+
+ database:
+ dsn: "host=47.84.63.130 user=postgres password=D@tkhong9 dbname=video_db port=5432 sslmode=disable"
+
+ redis:
+ addr: "47.84.62.226:6379"
+ password: "pass123"
+ db: 3
+
+ google:
+ client_id: "781933579930-avgrrvdj26ajqujs0snajk62jgch2jl5.apps.googleusercontent.com"
+ client_secret: "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"
+ redirect_url: "https://hlstiktok.com/auth/google/callback"
+ state_ttl_minutes: 10
+
+ internal:
+ marker: "your-secret-marker"
+
+ email:
+ from: "no-reply@picpic.com"
+
+ aws:
+ region: "us-east-1"
+ bucket: ""
+ access_key: ""
+ secret_key: ""
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: stream-api-svc
+ namespace: stream-production
+ labels:
+ app: stream-api
+spec:
+ selector:
+ app: stream-api
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 9000
+ type: NodePort
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: stream-api-dep
+ namespace: stream-production
+ labels:
+ app: stream-api
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: stream-api
+ template:
+ metadata:
+ labels:
+ app: stream-api
+ spec:
+ # imagePullSecrets:
+ # - name: registry-production-secret
+ containers:
+ - name: stream-api
+ image: registry.awing.vn/stream-production/stream-api:$BUILD_NUMBER
+ ports:
+ - containerPort: 9000
+ volumeMounts:
+ - name: stream-api-config
+ mountPath: /config.yaml
+ subPath: config.yaml
+ volumes:
+ - name: stream-api-config
+ configMap:
+ name: stream-api-config
diff --git a/Dockerfile b/Dockerfile
index 25c5fb4..bba627f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,14 +3,14 @@ FROM golang:1.25.6-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
-
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o main ./cmd/grpc
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o server ./cmd/server
+
FROM scratch
-COPY --from=builder /app/main /main
+COPY --from=builder /app/server /server
-EXPOSE 8080
-ENTRYPOINT ["/main"]
\ No newline at end of file
+EXPOSE 9000
+ENTRYPOINT ["/server"]
diff --git a/Dockerfile.agent b/Dockerfile.agent
new file mode 100644
index 0000000..7fd5e30
--- /dev/null
+++ b/Dockerfile.agent
@@ -0,0 +1,16 @@
+FROM golang:1.25.6-alpine AS builder
+
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o agent ./cmd/agent
+
+FROM alpine:latest
+
+RUN apk add --no-cache docker-cli
+
+COPY --from=builder /app/agent /bin/agent
+
+CMD ["/bin/agent"]
diff --git a/cmd/agent/main.go b/cmd/agent/main.go
new file mode 100644
index 0000000..22cf81d
--- /dev/null
+++ b/cmd/agent/main.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "context"
+ "log"
+ "os"
+ "os/signal"
+ "runtime"
+ "syscall"
+
+ "stream.api/internal/workflow/agent"
+)
+
+func main() {
+ serverAddr := os.Getenv("AGENT_SERVER")
+ if serverAddr == "" {
+ serverAddr = "localhost:9000"
+ }
+
+ secret := os.Getenv("APP_INTERNAL_MARKER")
+ if secret == "" {
+ secret = os.Getenv("INTERNAL_MARKER")
+ }
+ if secret == "" {
+ log.Fatal("APP_INTERNAL_MARKER environment variable is required")
+ }
+
+ capacity := runtime.NumCPU()
+
+ log.Printf("Starting stream.api agent")
+ log.Printf("Server: %s", serverAddr)
+ log.Printf("Capacity: %d", capacity)
+
+ a, err := agent.New(serverAddr, secret, capacity)
+ if err != nil {
+ log.Fatalf("Failed to initialize agent: %v", err)
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ <-sigCh
+ log.Println("Shutting down agent...")
+ cancel()
+ }()
+
+ if err := a.Run(ctx); err != nil {
+ log.Fatalf("Agent error: %v", err)
+ }
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 5f31b93..0e8f29a 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -45,7 +45,10 @@ func main() {
// 4. Initialize Components
appLogger := logger.NewLogger(cfg.Server.Mode)
- module, err := grpc.NewGRPCModule(context.Background(), cfg, db, rdb, appLogger)
+ appCtx, appCancel := context.WithCancel(context.Background())
+ defer appCancel()
+
+ module, err := grpc.NewGRPCModule(appCtx, cfg, db, rdb, appLogger)
if err != nil {
log.Fatalf("Failed to setup gRPC runtime module: %v", err)
}
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
deleted file mode 100644
index 4df0094..0000000
--- a/cmd/worker/main.go
+++ /dev/null
@@ -1 +0,0 @@
-package worker
diff --git a/config.example.yaml b/config.example.yaml
index 58ebc1b..eebc976 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -1,5 +1,4 @@
server:
- port: "8080"
grpc_port: "9000"
mode: "debug" # debug or release
@@ -11,28 +10,15 @@ redis:
password: ""
db: 0
-jwt:
- secret: "your_super_secret_jwt_key"
-
google:
client_id: "your_google_client_id"
client_secret: "your_google_client_secret"
redirect_url: "http://localhost:8080/auth/google/callback"
state_ttl_minutes: 10
-frontend:
- base_url: "http://localhost:5173"
- google_auth_finalize_path: "/auth/google/finalize"
-
internal:
marker: "your_shared_internal_auth_marker"
-cors:
- allow_origins:
- - "http://localhost:5173"
- - "http://localhost:8080"
- - "http://localhost:8081"
-
email:
from: "no-reply@picpic.com"
@@ -43,8 +29,11 @@ aws:
secret_key: "your_secret_key"
render:
- agent_secret: "your_render_agent_secret"
enable_metrics: true
enable_tracing: false
otlp_endpoint: ""
service_name: "stream-api-render"
+
+# Agent runtime uses environment variables rather than this YAML file.
+# Required: APP_INTERNAL_MARKER
+# Optional: AGENT_SERVER (default localhost:9000), FORCE_NEW_ID, AGENT_IMAGE, HOST_DOCKER_SOCK
diff --git a/config.yaml b/config.yaml
index 59a40fd..e35d905 100644
--- a/config.yaml
+++ b/config.yaml
@@ -10,16 +10,19 @@ redis:
password: "pass123"
db: 3
-jwt:
- secret: "your_super_secret_jwt_key"
-
google:
- client_id: "your_google_client_id"
- client_secret: "your_google_client_secret"
- redirect_url: "http://localhost:8080/auth/google/callback"
+ client_id: "781933579930-avgrrvdj26ajqujs0snajk62jgch2jl5.apps.googleusercontent.com"
+ client_secret: "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"
+ redirect_url: "https://hlstiktok.com/auth/google/callback"
email:
from: "no-reply@picpic.com"
internal:
marker: "your-secret-marker"
+
+aws:
+ region: "us-east-1"
+ bucket: "your-bucket-name"
+ access_key: "your_access_key"
+ secret_key: "your_secret_key"
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 8c29700..ad189ce 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1
+ github.com/docker/docker v26.1.5+incompatible
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/google/uuid v1.6.0
github.com/lib/pq v1.11.2
@@ -26,10 +27,33 @@ require (
)
require (
+ github.com/Microsoft/go-winio v0.4.21 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/term v0.5.2 // indirect
+ github.com/morikuni/aec v1.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
+ go.opentelemetry.io/otel v1.42.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
+ go.opentelemetry.io/otel/metric v1.42.0 // indirect
+ go.opentelemetry.io/otel/trace v1.42.0 // indirect
+ golang.org/x/time v0.15.0 // indirect
+ gotest.tools/v3 v3.5.2 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
@@ -67,7 +91,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
- github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sony/gobreaker v1.0.0
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -78,12 +101,12 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
- golang.org/x/net v0.50.0 // indirect
+ golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
gorm.io/datatypes v1.2.4
gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/hints v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index b12b890..f87dfd5 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,10 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
+github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
@@ -44,21 +48,36 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
+github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -68,6 +87,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -82,6 +103,8 @@ 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -97,6 +120,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -110,10 +135,22 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
+github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
@@ -124,6 +161,9 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@@ -137,24 +177,35 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
-go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
-go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
-go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
-go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
-go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
-go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
-go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
-go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
-go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
+go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
+go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
+go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
+go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
+go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
+go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
+go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
+go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
+go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
+go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
@@ -163,27 +214,58 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
+golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -216,6 +298,8 @@ gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw=
gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
diff --git a/internal/adapters/redis/adapter.go b/internal/adapters/redis/adapter.go
index bc8b646..b9400d4 100644
--- a/internal/adapters/redis/adapter.go
+++ b/internal/adapters/redis/adapter.go
@@ -3,7 +3,9 @@ package redis
import (
"context"
"encoding/json"
+ "errors"
"fmt"
+ "strings"
"time"
goredis "github.com/redis/go-redis/v9"
@@ -12,14 +14,24 @@ import (
)
const (
- JobQueueKey = "render:jobs:queue"
- LogChannel = "render:jobs:logs"
- ResourceChannel = "render:agents:resources"
- JobUpdateChannel = "render:jobs:updates"
+ JobQueueKey = "render:jobs:queue:v2"
+ JobInflightKey = "render:jobs:inflight"
+ JobInflightMetaKey = "render:jobs:inflight:meta"
+ JobSequenceKey = "render:jobs:queue:seq"
+ LogChannel = "render:jobs:logs"
+ ResourceChannel = "render:agents:resources"
+ JobUpdateChannel = "render:jobs:updates"
+ defaultQueuePoll = time.Second
+ defaultInflightTTL = 15 * time.Minute
)
type RedisAdapter struct{ client *goredis.Client }
+type inflightMeta struct {
+ ReadyScore float64 `json:"ready_score"`
+ ClaimedAt int64 `json:"claimed_at"`
+}
+
func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db})
if err := client.Ping(context.Background()).Err(); err != nil {
@@ -31,13 +43,26 @@ func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
func (r *RedisAdapter) Client() *goredis.Client { return r.client }
func (r *RedisAdapter) Enqueue(ctx context.Context, job *model.Job) error {
- data, err := json.Marshal(job)
+ if job == nil || strings.TrimSpace(job.ID) == "" {
+ return errors.New("job id is required")
+ }
+ priority := int64(0)
+ if job.Priority != nil {
+ priority = *job.Priority
+ }
+ seq, err := r.client.Incr(ctx, JobSequenceKey).Result()
if err != nil {
return err
}
- timestamp := time.Now().UnixNano()
- score := float64(-(int64(*job.Priority) * 1000000000) - timestamp)
- return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err()
+ score := float64((-priority * 1_000_000_000_000) + seq)
+ jobID := strings.TrimSpace(job.ID)
+ if err := r.client.HDel(ctx, JobInflightMetaKey, jobID).Err(); err != nil {
+ return err
+ }
+ if err := r.client.ZRem(ctx, JobInflightKey, jobID).Err(); err != nil {
+ return err
+ }
+ return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: jobID}).Err()
}
func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
@@ -56,27 +81,57 @@ func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
- case <-time.After(time.Second):
+ case <-time.After(defaultQueuePoll):
continue
}
}
- var raw []byte
- switch member := res[0].Member.(type) {
- case string:
- raw = []byte(member)
- case []byte:
- raw = member
- default:
- return nil, fmt.Errorf("unexpected redis queue payload type %T", member)
- }
- var job model.Job
- if err := json.Unmarshal(raw, &job); err != nil {
+ jobID := fmt.Sprintf("%v", res[0].Member)
+ meta, err := json.Marshal(inflightMeta{ReadyScore: res[0].Score, ClaimedAt: time.Now().Unix()})
+ if err != nil {
return nil, err
}
- return &job, nil
+ leaseScore := float64(time.Now().Add(defaultInflightTTL).Unix())
+ pipe := r.client.TxPipeline()
+ pipe.ZAdd(ctx, JobInflightKey, goredis.Z{Score: leaseScore, Member: jobID})
+ pipe.HSet(ctx, JobInflightMetaKey, jobID, meta)
+ if _, err := pipe.Exec(ctx); err != nil {
+ return nil, err
+ }
+ return &model.Job{ID: jobID}, nil
}
}
+func (r *RedisAdapter) Ack(ctx context.Context, jobID string) error {
+ jobID = strings.TrimSpace(jobID)
+ if jobID == "" {
+ return nil
+ }
+ pipe := r.client.TxPipeline()
+ pipe.ZRem(ctx, JobQueueKey, jobID)
+ pipe.ZRem(ctx, JobInflightKey, jobID)
+ pipe.HDel(ctx, JobInflightMetaKey, jobID)
+ _, err := pipe.Exec(ctx)
+ return err
+}
+
+func (r *RedisAdapter) ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error) {
+ if limit <= 0 {
+ limit = 100
+ }
+ return r.client.ZRangeByScore(ctx, JobInflightKey, &goredis.ZRangeBy{Min: "-inf", Max: fmt.Sprintf("%d", now.Unix()), Offset: 0, Count: limit}).Result()
+}
+
+func (r *RedisAdapter) TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error {
+ jobID = strings.TrimSpace(jobID)
+ if jobID == "" {
+ return nil
+ }
+ if ttl <= 0 {
+ ttl = defaultInflightTTL
+ }
+ return r.client.ZAddXX(ctx, JobInflightKey, goredis.Z{Score: float64(time.Now().Add(ttl).Unix()), Member: jobID}).Err()
+}
+
func (r *RedisAdapter) Publish(ctx context.Context, jobID string, logLine string, progress float64) error {
payload, err := json.Marshal(dto.LogEntry{JobID: jobID, Line: logLine, Progress: progress})
if err != nil {
@@ -173,6 +228,7 @@ func (r *RedisAdapter) SubscribeJobUpdates(ctx context.Context) (<-chan string,
}()
return ch, nil
}
+
func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
return c.client.Set(ctx, key, value, expiration).Err()
}
diff --git a/internal/adapters/redis/dlq.go b/internal/adapters/redis/dlq.go
index 6008b81..d028618 100644
--- a/internal/adapters/redis/dlq.go
+++ b/internal/adapters/redis/dlq.go
@@ -12,8 +12,8 @@ import (
)
const (
- dlqKey = "picpic:dlq"
- dlqMetaPrefix = "picpic:dlq:meta:"
+ dlqKey = "render:jobs:dlq"
+ dlqMetaPrefix = "render:jobs:dlq:meta:"
)
type DeadLetterQueue struct {
@@ -33,11 +33,16 @@ func NewDeadLetterQueue(client *redis.Client) *DeadLetterQueue {
// Add adds a failed job to the DLQ
func (dlq *DeadLetterQueue) Add(ctx context.Context, job *model.Job, reason string) error {
+ retryCount := int64(0)
+ if job != nil && job.RetryCount != nil {
+ retryCount = *job.RetryCount
+ }
+
entry := DLQEntry{
Job: job,
FailureTime: time.Now(),
Reason: reason,
- RetryCount: *job.RetryCount,
+ RetryCount: retryCount,
}
data, err := json.Marshal(entry)
diff --git a/internal/api/proto/app/v1/admin.pb.go b/internal/api/proto/app/v1/admin.pb.go
index 9dd0f57..6c8a1c0 100644
--- a/internal/api/proto/app/v1/admin.pb.go
+++ b/internal/api/proto/app/v1/admin.pb.go
@@ -4803,6 +4803,398 @@ func (x *RetryAdminJobResponse) GetJob() *AdminJob {
return nil
}
+type ListAdminDlqJobsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Offset int32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"`
+ Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListAdminDlqJobsRequest) Reset() {
+ *x = ListAdminDlqJobsRequest{}
+ mi := &file_app_v1_admin_proto_msgTypes[79]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListAdminDlqJobsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAdminDlqJobsRequest) ProtoMessage() {}
+
+func (x *ListAdminDlqJobsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[79]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAdminDlqJobsRequest.ProtoReflect.Descriptor instead.
+func (*ListAdminDlqJobsRequest) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{79}
+}
+
+func (x *ListAdminDlqJobsRequest) GetOffset() int32 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+func (x *ListAdminDlqJobsRequest) GetLimit() int32 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type ListAdminDlqJobsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Items []*AdminDlqEntry `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
+ Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
+ Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"`
+ Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListAdminDlqJobsResponse) Reset() {
+ *x = ListAdminDlqJobsResponse{}
+ mi := &file_app_v1_admin_proto_msgTypes[80]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListAdminDlqJobsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAdminDlqJobsResponse) ProtoMessage() {}
+
+func (x *ListAdminDlqJobsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[80]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAdminDlqJobsResponse.ProtoReflect.Descriptor instead.
+func (*ListAdminDlqJobsResponse) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{80}
+}
+
+func (x *ListAdminDlqJobsResponse) GetItems() []*AdminDlqEntry {
+ if x != nil {
+ return x.Items
+ }
+ return nil
+}
+
+func (x *ListAdminDlqJobsResponse) GetTotal() int64 {
+ if x != nil {
+ return x.Total
+ }
+ return 0
+}
+
+func (x *ListAdminDlqJobsResponse) GetOffset() int32 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+func (x *ListAdminDlqJobsResponse) GetLimit() int32 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+type GetAdminDlqJobRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetAdminDlqJobRequest) Reset() {
+ *x = GetAdminDlqJobRequest{}
+ mi := &file_app_v1_admin_proto_msgTypes[81]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetAdminDlqJobRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetAdminDlqJobRequest) ProtoMessage() {}
+
+func (x *GetAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[81]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetAdminDlqJobRequest.ProtoReflect.Descriptor instead.
+func (*GetAdminDlqJobRequest) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{81}
+}
+
+func (x *GetAdminDlqJobRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type GetAdminDlqJobResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Item *AdminDlqEntry `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetAdminDlqJobResponse) Reset() {
+ *x = GetAdminDlqJobResponse{}
+ mi := &file_app_v1_admin_proto_msgTypes[82]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetAdminDlqJobResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetAdminDlqJobResponse) ProtoMessage() {}
+
+func (x *GetAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[82]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetAdminDlqJobResponse.ProtoReflect.Descriptor instead.
+func (*GetAdminDlqJobResponse) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{82}
+}
+
+func (x *GetAdminDlqJobResponse) GetItem() *AdminDlqEntry {
+ if x != nil {
+ return x.Item
+ }
+ return nil
+}
+
+type RetryAdminDlqJobRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RetryAdminDlqJobRequest) Reset() {
+ *x = RetryAdminDlqJobRequest{}
+ mi := &file_app_v1_admin_proto_msgTypes[83]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RetryAdminDlqJobRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RetryAdminDlqJobRequest) ProtoMessage() {}
+
+func (x *RetryAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[83]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RetryAdminDlqJobRequest.ProtoReflect.Descriptor instead.
+func (*RetryAdminDlqJobRequest) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{83}
+}
+
+func (x *RetryAdminDlqJobRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type RetryAdminDlqJobResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RetryAdminDlqJobResponse) Reset() {
+ *x = RetryAdminDlqJobResponse{}
+ mi := &file_app_v1_admin_proto_msgTypes[84]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RetryAdminDlqJobResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RetryAdminDlqJobResponse) ProtoMessage() {}
+
+func (x *RetryAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[84]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RetryAdminDlqJobResponse.ProtoReflect.Descriptor instead.
+func (*RetryAdminDlqJobResponse) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{84}
+}
+
+func (x *RetryAdminDlqJobResponse) GetJob() *AdminJob {
+ if x != nil {
+ return x.Job
+ }
+ return nil
+}
+
+type RemoveAdminDlqJobRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveAdminDlqJobRequest) Reset() {
+ *x = RemoveAdminDlqJobRequest{}
+ mi := &file_app_v1_admin_proto_msgTypes[85]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveAdminDlqJobRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveAdminDlqJobRequest) ProtoMessage() {}
+
+func (x *RemoveAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[85]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RemoveAdminDlqJobRequest.ProtoReflect.Descriptor instead.
+func (*RemoveAdminDlqJobRequest) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{85}
+}
+
+func (x *RemoveAdminDlqJobRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type RemoveAdminDlqJobResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
+ JobId string `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveAdminDlqJobResponse) Reset() {
+ *x = RemoveAdminDlqJobResponse{}
+ mi := &file_app_v1_admin_proto_msgTypes[86]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveAdminDlqJobResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveAdminDlqJobResponse) ProtoMessage() {}
+
+func (x *RemoveAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_admin_proto_msgTypes[86]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RemoveAdminDlqJobResponse.ProtoReflect.Descriptor instead.
+func (*RemoveAdminDlqJobResponse) Descriptor() ([]byte, []int) {
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{86}
+}
+
+func (x *RemoveAdminDlqJobResponse) GetStatus() string {
+ if x != nil {
+ return x.Status
+ }
+ return ""
+}
+
+func (x *RemoveAdminDlqJobResponse) GetJobId() string {
+ if x != nil {
+ return x.JobId
+ }
+ return ""
+}
+
type ListAdminAgentsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -4811,7 +5203,7 @@ type ListAdminAgentsRequest struct {
func (x *ListAdminAgentsRequest) Reset() {
*x = ListAdminAgentsRequest{}
- mi := &file_app_v1_admin_proto_msgTypes[79]
+ mi := &file_app_v1_admin_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4823,7 +5215,7 @@ func (x *ListAdminAgentsRequest) String() string {
func (*ListAdminAgentsRequest) ProtoMessage() {}
func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_app_v1_admin_proto_msgTypes[79]
+ mi := &file_app_v1_admin_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4836,7 +5228,7 @@ func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsRequest.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsRequest) Descriptor() ([]byte, []int) {
- return file_app_v1_admin_proto_rawDescGZIP(), []int{79}
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{87}
}
type ListAdminAgentsResponse struct {
@@ -4848,7 +5240,7 @@ type ListAdminAgentsResponse struct {
func (x *ListAdminAgentsResponse) Reset() {
*x = ListAdminAgentsResponse{}
- mi := &file_app_v1_admin_proto_msgTypes[80]
+ mi := &file_app_v1_admin_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4860,7 +5252,7 @@ func (x *ListAdminAgentsResponse) String() string {
func (*ListAdminAgentsResponse) ProtoMessage() {}
func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_app_v1_admin_proto_msgTypes[80]
+ mi := &file_app_v1_admin_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4873,7 +5265,7 @@ func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsResponse.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsResponse) Descriptor() ([]byte, []int) {
- return file_app_v1_admin_proto_rawDescGZIP(), []int{80}
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{88}
}
func (x *ListAdminAgentsResponse) GetAgents() []*AdminAgent {
@@ -4892,7 +5284,7 @@ type RestartAdminAgentRequest struct {
func (x *RestartAdminAgentRequest) Reset() {
*x = RestartAdminAgentRequest{}
- mi := &file_app_v1_admin_proto_msgTypes[81]
+ mi := &file_app_v1_admin_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4904,7 +5296,7 @@ func (x *RestartAdminAgentRequest) String() string {
func (*RestartAdminAgentRequest) ProtoMessage() {}
func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message {
- mi := &file_app_v1_admin_proto_msgTypes[81]
+ mi := &file_app_v1_admin_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4917,7 +5309,7 @@ func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*RestartAdminAgentRequest) Descriptor() ([]byte, []int) {
- return file_app_v1_admin_proto_rawDescGZIP(), []int{81}
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{89}
}
func (x *RestartAdminAgentRequest) GetId() string {
@@ -4936,7 +5328,7 @@ type UpdateAdminAgentRequest struct {
func (x *UpdateAdminAgentRequest) Reset() {
*x = UpdateAdminAgentRequest{}
- mi := &file_app_v1_admin_proto_msgTypes[82]
+ mi := &file_app_v1_admin_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4948,7 +5340,7 @@ func (x *UpdateAdminAgentRequest) String() string {
func (*UpdateAdminAgentRequest) ProtoMessage() {}
func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message {
- mi := &file_app_v1_admin_proto_msgTypes[82]
+ mi := &file_app_v1_admin_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4961,7 +5353,7 @@ func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*UpdateAdminAgentRequest) Descriptor() ([]byte, []int) {
- return file_app_v1_admin_proto_rawDescGZIP(), []int{82}
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{90}
}
func (x *UpdateAdminAgentRequest) GetId() string {
@@ -4980,7 +5372,7 @@ type AdminAgentCommandResponse struct {
func (x *AdminAgentCommandResponse) Reset() {
*x = AdminAgentCommandResponse{}
- mi := &file_app_v1_admin_proto_msgTypes[83]
+ mi := &file_app_v1_admin_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4992,7 +5384,7 @@ func (x *AdminAgentCommandResponse) String() string {
func (*AdminAgentCommandResponse) ProtoMessage() {}
func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message {
- mi := &file_app_v1_admin_proto_msgTypes[83]
+ mi := &file_app_v1_admin_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5005,7 +5397,7 @@ func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminAgentCommandResponse.ProtoReflect.Descriptor instead.
func (*AdminAgentCommandResponse) Descriptor() ([]byte, []int) {
- return file_app_v1_admin_proto_rawDescGZIP(), []int{83}
+ return file_app_v1_admin_proto_rawDescGZIP(), []int{91}
}
func (x *AdminAgentCommandResponse) GetStatus() string {
@@ -5448,7 +5840,28 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x14RetryAdminJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"B\n" +
"\x15RetryAdminJobResponse\x12)\n" +
- "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"\x18\n" +
+ "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"G\n" +
+ "\x17ListAdminDlqJobsRequest\x12\x16\n" +
+ "\x06offset\x18\x01 \x01(\x05R\x06offset\x12\x14\n" +
+ "\x05limit\x18\x02 \x01(\x05R\x05limit\"\x92\x01\n" +
+ "\x18ListAdminDlqJobsResponse\x122\n" +
+ "\x05items\x18\x01 \x03(\v2\x1c.stream.app.v1.AdminDlqEntryR\x05items\x12\x14\n" +
+ "\x05total\x18\x02 \x01(\x03R\x05total\x12\x16\n" +
+ "\x06offset\x18\x03 \x01(\x05R\x06offset\x12\x14\n" +
+ "\x05limit\x18\x04 \x01(\x05R\x05limit\"'\n" +
+ "\x15GetAdminDlqJobRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"J\n" +
+ "\x16GetAdminDlqJobResponse\x120\n" +
+ "\x04item\x18\x01 \x01(\v2\x1c.stream.app.v1.AdminDlqEntryR\x04item\")\n" +
+ "\x17RetryAdminDlqJobRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"E\n" +
+ "\x18RetryAdminDlqJobResponse\x12)\n" +
+ "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"*\n" +
+ "\x18RemoveAdminDlqJobRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"J\n" +
+ "\x19RemoveAdminDlqJobResponse\x12\x16\n" +
+ "\x06status\x18\x01 \x01(\tR\x06status\x12\x15\n" +
+ "\x06job_id\x18\x02 \x01(\tR\x05jobId\"\x18\n" +
"\x16ListAdminAgentsRequest\"L\n" +
"\x17ListAdminAgentsResponse\x121\n" +
"\x06agents\x18\x01 \x03(\v2\x19.stream.app.v1.AdminAgentR\x06agents\"*\n" +
@@ -5457,7 +5870,7 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x17UpdateAdminAgentRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"3\n" +
"\x19AdminAgentCommandResponse\x12\x16\n" +
- "\x06status\x18\x01 \x01(\tR\x06status2\x9d$\n" +
+ "\x06status\x18\x01 \x01(\tR\x06status2\xae'\n" +
"\x05Admin\x12f\n" +
"\x11GetAdminDashboard\x12'.stream.app.v1.GetAdminDashboardRequest\x1a(.stream.app.v1.GetAdminDashboardResponse\x12]\n" +
"\x0eListAdminUsers\x12$.stream.app.v1.ListAdminUsersRequest\x1a%.stream.app.v1.ListAdminUsersResponse\x12W\n" +
@@ -5500,7 +5913,11 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x0fGetAdminJobLogs\x12%.stream.app.v1.GetAdminJobLogsRequest\x1a&.stream.app.v1.GetAdminJobLogsResponse\x12]\n" +
"\x0eCreateAdminJob\x12$.stream.app.v1.CreateAdminJobRequest\x1a%.stream.app.v1.CreateAdminJobResponse\x12]\n" +
"\x0eCancelAdminJob\x12$.stream.app.v1.CancelAdminJobRequest\x1a%.stream.app.v1.CancelAdminJobResponse\x12Z\n" +
- "\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12`\n" +
+ "\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12c\n" +
+ "\x10ListAdminDlqJobs\x12&.stream.app.v1.ListAdminDlqJobsRequest\x1a'.stream.app.v1.ListAdminDlqJobsResponse\x12]\n" +
+ "\x0eGetAdminDlqJob\x12$.stream.app.v1.GetAdminDlqJobRequest\x1a%.stream.app.v1.GetAdminDlqJobResponse\x12c\n" +
+ "\x10RetryAdminDlqJob\x12&.stream.app.v1.RetryAdminDlqJobRequest\x1a'.stream.app.v1.RetryAdminDlqJobResponse\x12f\n" +
+ "\x11RemoveAdminDlqJob\x12'.stream.app.v1.RemoveAdminDlqJobRequest\x1a(.stream.app.v1.RemoveAdminDlqJobResponse\x12`\n" +
"\x0fListAdminAgents\x12%.stream.app.v1.ListAdminAgentsRequest\x1a&.stream.app.v1.ListAdminAgentsResponse\x12f\n" +
"\x11RestartAdminAgent\x12'.stream.app.v1.RestartAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponse\x12d\n" +
"\x10UpdateAdminAgent\x12&.stream.app.v1.UpdateAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
@@ -5517,7 +5934,7 @@ func file_app_v1_admin_proto_rawDescGZIP() []byte {
return file_app_v1_admin_proto_rawDescData
}
-var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 85)
+var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 93)
var file_app_v1_admin_proto_goTypes = []any{
(*GetAdminDashboardRequest)(nil), // 0: stream.app.v1.GetAdminDashboardRequest
(*GetAdminDashboardResponse)(nil), // 1: stream.app.v1.GetAdminDashboardResponse
@@ -5598,158 +6015,178 @@ var file_app_v1_admin_proto_goTypes = []any{
(*CancelAdminJobResponse)(nil), // 76: stream.app.v1.CancelAdminJobResponse
(*RetryAdminJobRequest)(nil), // 77: stream.app.v1.RetryAdminJobRequest
(*RetryAdminJobResponse)(nil), // 78: stream.app.v1.RetryAdminJobResponse
- (*ListAdminAgentsRequest)(nil), // 79: stream.app.v1.ListAdminAgentsRequest
- (*ListAdminAgentsResponse)(nil), // 80: stream.app.v1.ListAdminAgentsResponse
- (*RestartAdminAgentRequest)(nil), // 81: stream.app.v1.RestartAdminAgentRequest
- (*UpdateAdminAgentRequest)(nil), // 82: stream.app.v1.UpdateAdminAgentRequest
- (*AdminAgentCommandResponse)(nil), // 83: stream.app.v1.AdminAgentCommandResponse
- nil, // 84: stream.app.v1.CreateAdminJobRequest.EnvEntry
- (*AdminDashboard)(nil), // 85: stream.app.v1.AdminDashboard
- (*AdminUser)(nil), // 86: stream.app.v1.AdminUser
- (*AdminUserDetail)(nil), // 87: stream.app.v1.AdminUserDetail
- (*AdminVideo)(nil), // 88: stream.app.v1.AdminVideo
- (*AdminPayment)(nil), // 89: stream.app.v1.AdminPayment
- (*PlanSubscription)(nil), // 90: stream.app.v1.PlanSubscription
- (*AdminPlan)(nil), // 91: stream.app.v1.AdminPlan
- (*AdminAdTemplate)(nil), // 92: stream.app.v1.AdminAdTemplate
- (*AdminPopupAd)(nil), // 93: stream.app.v1.AdminPopupAd
- (*AdminPlayerConfig)(nil), // 94: stream.app.v1.AdminPlayerConfig
- (*AdminJob)(nil), // 95: stream.app.v1.AdminJob
- (*AdminAgent)(nil), // 96: stream.app.v1.AdminAgent
- (*MessageResponse)(nil), // 97: stream.app.v1.MessageResponse
+ (*ListAdminDlqJobsRequest)(nil), // 79: stream.app.v1.ListAdminDlqJobsRequest
+ (*ListAdminDlqJobsResponse)(nil), // 80: stream.app.v1.ListAdminDlqJobsResponse
+ (*GetAdminDlqJobRequest)(nil), // 81: stream.app.v1.GetAdminDlqJobRequest
+ (*GetAdminDlqJobResponse)(nil), // 82: stream.app.v1.GetAdminDlqJobResponse
+ (*RetryAdminDlqJobRequest)(nil), // 83: stream.app.v1.RetryAdminDlqJobRequest
+ (*RetryAdminDlqJobResponse)(nil), // 84: stream.app.v1.RetryAdminDlqJobResponse
+ (*RemoveAdminDlqJobRequest)(nil), // 85: stream.app.v1.RemoveAdminDlqJobRequest
+ (*RemoveAdminDlqJobResponse)(nil), // 86: stream.app.v1.RemoveAdminDlqJobResponse
+ (*ListAdminAgentsRequest)(nil), // 87: stream.app.v1.ListAdminAgentsRequest
+ (*ListAdminAgentsResponse)(nil), // 88: stream.app.v1.ListAdminAgentsResponse
+ (*RestartAdminAgentRequest)(nil), // 89: stream.app.v1.RestartAdminAgentRequest
+ (*UpdateAdminAgentRequest)(nil), // 90: stream.app.v1.UpdateAdminAgentRequest
+ (*AdminAgentCommandResponse)(nil), // 91: stream.app.v1.AdminAgentCommandResponse
+ nil, // 92: stream.app.v1.CreateAdminJobRequest.EnvEntry
+ (*AdminDashboard)(nil), // 93: stream.app.v1.AdminDashboard
+ (*AdminUser)(nil), // 94: stream.app.v1.AdminUser
+ (*AdminUserDetail)(nil), // 95: stream.app.v1.AdminUserDetail
+ (*AdminVideo)(nil), // 96: stream.app.v1.AdminVideo
+ (*AdminPayment)(nil), // 97: stream.app.v1.AdminPayment
+ (*PlanSubscription)(nil), // 98: stream.app.v1.PlanSubscription
+ (*AdminPlan)(nil), // 99: stream.app.v1.AdminPlan
+ (*AdminAdTemplate)(nil), // 100: stream.app.v1.AdminAdTemplate
+ (*AdminPopupAd)(nil), // 101: stream.app.v1.AdminPopupAd
+ (*AdminPlayerConfig)(nil), // 102: stream.app.v1.AdminPlayerConfig
+ (*AdminJob)(nil), // 103: stream.app.v1.AdminJob
+ (*AdminDlqEntry)(nil), // 104: stream.app.v1.AdminDlqEntry
+ (*AdminAgent)(nil), // 105: stream.app.v1.AdminAgent
+ (*MessageResponse)(nil), // 106: stream.app.v1.MessageResponse
}
var file_app_v1_admin_proto_depIdxs = []int32{
- 85, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard
- 86, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser
- 87, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail
- 86, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
- 86, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
- 87, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail
- 88, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo
- 88, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
- 88, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
- 88, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
- 89, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment
- 89, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
- 89, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
- 90, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription
- 89, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
- 91, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan
- 91, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
- 91, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
- 92, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate
- 92, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
- 92, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
- 92, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
- 93, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd
- 93, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
- 93, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
- 93, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
- 94, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig
- 94, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
- 94, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
- 94, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
- 95, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob
- 95, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
- 84, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry
- 95, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
- 95, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
- 96, // 35: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent
- 0, // 36: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest
- 2, // 37: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest
- 4, // 38: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest
- 6, // 39: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest
- 8, // 40: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest
- 10, // 41: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest
- 12, // 42: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest
- 14, // 43: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest
- 15, // 44: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest
- 17, // 45: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest
- 19, // 46: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest
- 21, // 47: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest
- 23, // 48: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest
- 24, // 49: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest
- 26, // 50: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest
- 28, // 51: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest
- 30, // 52: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest
- 32, // 53: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest
- 34, // 54: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest
- 36, // 55: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest
- 38, // 56: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest
- 40, // 57: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest
- 42, // 58: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest
- 44, // 59: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest
- 46, // 60: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest
- 48, // 61: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest
- 49, // 62: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest
- 51, // 63: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest
- 53, // 64: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest
- 55, // 65: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest
- 57, // 66: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest
- 58, // 67: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest
- 60, // 68: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest
- 62, // 69: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest
- 64, // 70: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest
- 66, // 71: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest
- 67, // 72: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest
- 69, // 73: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest
- 71, // 74: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest
- 73, // 75: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest
- 75, // 76: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest
- 77, // 77: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest
- 79, // 78: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest
- 81, // 79: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest
- 82, // 80: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest
- 1, // 81: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse
- 3, // 82: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse
- 5, // 83: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse
- 7, // 84: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse
- 9, // 85: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse
- 11, // 86: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse
- 13, // 87: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse
- 97, // 88: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse
- 16, // 89: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse
- 18, // 90: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse
- 20, // 91: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse
- 22, // 92: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse
- 97, // 93: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse
- 25, // 94: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse
- 27, // 95: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse
- 29, // 96: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse
- 31, // 97: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse
- 33, // 98: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse
- 35, // 99: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse
- 37, // 100: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse
- 39, // 101: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse
- 41, // 102: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse
- 43, // 103: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse
- 45, // 104: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse
- 47, // 105: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse
- 97, // 106: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse
- 50, // 107: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse
- 52, // 108: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse
- 54, // 109: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse
- 56, // 110: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse
- 97, // 111: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse
- 59, // 112: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse
- 61, // 113: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse
- 63, // 114: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse
- 65, // 115: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse
- 97, // 116: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse
- 68, // 117: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse
- 70, // 118: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse
- 72, // 119: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse
- 74, // 120: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse
- 76, // 121: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse
- 78, // 122: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse
- 80, // 123: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse
- 83, // 124: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
- 83, // 125: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
- 81, // [81:126] is the sub-list for method output_type
- 36, // [36:81] is the sub-list for method input_type
- 36, // [36:36] is the sub-list for extension type_name
- 36, // [36:36] is the sub-list for extension extendee
- 0, // [0:36] is the sub-list for field type_name
+ 93, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard
+ 94, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser
+ 95, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail
+ 94, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
+ 94, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
+ 95, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail
+ 96, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo
+ 96, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
+ 96, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
+ 96, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
+ 97, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment
+ 97, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
+ 97, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
+ 98, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription
+ 97, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
+ 99, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan
+ 99, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
+ 99, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
+ 100, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate
+ 100, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
+ 100, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
+ 100, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
+ 101, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd
+ 101, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
+ 101, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
+ 101, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
+ 102, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig
+ 102, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
+ 102, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
+ 102, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
+ 103, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob
+ 103, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
+ 92, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry
+ 103, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
+ 103, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
+ 104, // 35: stream.app.v1.ListAdminDlqJobsResponse.items:type_name -> stream.app.v1.AdminDlqEntry
+ 104, // 36: stream.app.v1.GetAdminDlqJobResponse.item:type_name -> stream.app.v1.AdminDlqEntry
+ 103, // 37: stream.app.v1.RetryAdminDlqJobResponse.job:type_name -> stream.app.v1.AdminJob
+ 105, // 38: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent
+ 0, // 39: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest
+ 2, // 40: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest
+ 4, // 41: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest
+ 6, // 42: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest
+ 8, // 43: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest
+ 10, // 44: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest
+ 12, // 45: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest
+ 14, // 46: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest
+ 15, // 47: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest
+ 17, // 48: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest
+ 19, // 49: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest
+ 21, // 50: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest
+ 23, // 51: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest
+ 24, // 52: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest
+ 26, // 53: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest
+ 28, // 54: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest
+ 30, // 55: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest
+ 32, // 56: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest
+ 34, // 57: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest
+ 36, // 58: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest
+ 38, // 59: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest
+ 40, // 60: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest
+ 42, // 61: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest
+ 44, // 62: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest
+ 46, // 63: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest
+ 48, // 64: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest
+ 49, // 65: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest
+ 51, // 66: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest
+ 53, // 67: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest
+ 55, // 68: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest
+ 57, // 69: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest
+ 58, // 70: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest
+ 60, // 71: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest
+ 62, // 72: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest
+ 64, // 73: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest
+ 66, // 74: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest
+ 67, // 75: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest
+ 69, // 76: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest
+ 71, // 77: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest
+ 73, // 78: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest
+ 75, // 79: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest
+ 77, // 80: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest
+ 79, // 81: stream.app.v1.Admin.ListAdminDlqJobs:input_type -> stream.app.v1.ListAdminDlqJobsRequest
+ 81, // 82: stream.app.v1.Admin.GetAdminDlqJob:input_type -> stream.app.v1.GetAdminDlqJobRequest
+ 83, // 83: stream.app.v1.Admin.RetryAdminDlqJob:input_type -> stream.app.v1.RetryAdminDlqJobRequest
+ 85, // 84: stream.app.v1.Admin.RemoveAdminDlqJob:input_type -> stream.app.v1.RemoveAdminDlqJobRequest
+ 87, // 85: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest
+ 89, // 86: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest
+ 90, // 87: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest
+ 1, // 88: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse
+ 3, // 89: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse
+ 5, // 90: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse
+ 7, // 91: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse
+ 9, // 92: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse
+ 11, // 93: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse
+ 13, // 94: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse
+ 106, // 95: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse
+ 16, // 96: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse
+ 18, // 97: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse
+ 20, // 98: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse
+ 22, // 99: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse
+ 106, // 100: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse
+ 25, // 101: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse
+ 27, // 102: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse
+ 29, // 103: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse
+ 31, // 104: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse
+ 33, // 105: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse
+ 35, // 106: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse
+ 37, // 107: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse
+ 39, // 108: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse
+ 41, // 109: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse
+ 43, // 110: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse
+ 45, // 111: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse
+ 47, // 112: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse
+ 106, // 113: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse
+ 50, // 114: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse
+ 52, // 115: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse
+ 54, // 116: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse
+ 56, // 117: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse
+ 106, // 118: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse
+ 59, // 119: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse
+ 61, // 120: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse
+ 63, // 121: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse
+ 65, // 122: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse
+ 106, // 123: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse
+ 68, // 124: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse
+ 70, // 125: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse
+ 72, // 126: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse
+ 74, // 127: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse
+ 76, // 128: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse
+ 78, // 129: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse
+ 80, // 130: stream.app.v1.Admin.ListAdminDlqJobs:output_type -> stream.app.v1.ListAdminDlqJobsResponse
+ 82, // 131: stream.app.v1.Admin.GetAdminDlqJob:output_type -> stream.app.v1.GetAdminDlqJobResponse
+ 84, // 132: stream.app.v1.Admin.RetryAdminDlqJob:output_type -> stream.app.v1.RetryAdminDlqJobResponse
+ 86, // 133: stream.app.v1.Admin.RemoveAdminDlqJob:output_type -> stream.app.v1.RemoveAdminDlqJobResponse
+ 88, // 134: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse
+ 91, // 135: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
+ 91, // 136: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
+ 88, // [88:137] is the sub-list for method output_type
+ 39, // [39:88] is the sub-list for method input_type
+ 39, // [39:39] is the sub-list for extension type_name
+ 39, // [39:39] is the sub-list for extension extendee
+ 0, // [0:39] is the sub-list for field type_name
}
func init() { file_app_v1_admin_proto_init() }
@@ -5787,7 +6224,7 @@ func file_app_v1_admin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_admin_proto_rawDesc), len(file_app_v1_admin_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 85,
+ NumMessages: 93,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/internal/api/proto/app/v1/admin_grpc.pb.go b/internal/api/proto/app/v1/admin_grpc.pb.go
index 0772c17..67f7c89 100644
--- a/internal/api/proto/app/v1/admin_grpc.pb.go
+++ b/internal/api/proto/app/v1/admin_grpc.pb.go
@@ -61,6 +61,10 @@ const (
Admin_CreateAdminJob_FullMethodName = "/stream.app.v1.Admin/CreateAdminJob"
Admin_CancelAdminJob_FullMethodName = "/stream.app.v1.Admin/CancelAdminJob"
Admin_RetryAdminJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminJob"
+ Admin_ListAdminDlqJobs_FullMethodName = "/stream.app.v1.Admin/ListAdminDlqJobs"
+ Admin_GetAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/GetAdminDlqJob"
+ Admin_RetryAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminDlqJob"
+ Admin_RemoveAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RemoveAdminDlqJob"
Admin_ListAdminAgents_FullMethodName = "/stream.app.v1.Admin/ListAdminAgents"
Admin_RestartAdminAgent_FullMethodName = "/stream.app.v1.Admin/RestartAdminAgent"
Admin_UpdateAdminAgent_FullMethodName = "/stream.app.v1.Admin/UpdateAdminAgent"
@@ -112,6 +116,10 @@ type AdminClient interface {
CreateAdminJob(ctx context.Context, in *CreateAdminJobRequest, opts ...grpc.CallOption) (*CreateAdminJobResponse, error)
CancelAdminJob(ctx context.Context, in *CancelAdminJobRequest, opts ...grpc.CallOption) (*CancelAdminJobResponse, error)
RetryAdminJob(ctx context.Context, in *RetryAdminJobRequest, opts ...grpc.CallOption) (*RetryAdminJobResponse, error)
+ ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error)
+ GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error)
+ RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error)
+ RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error)
ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error)
RestartAdminAgent(ctx context.Context, in *RestartAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(ctx context.Context, in *UpdateAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error)
@@ -545,6 +553,46 @@ func (c *adminClient) RetryAdminJob(ctx context.Context, in *RetryAdminJobReques
return out, nil
}
+func (c *adminClient) ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListAdminDlqJobsResponse)
+ err := c.cc.Invoke(ctx, Admin_ListAdminDlqJobs_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *adminClient) GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetAdminDlqJobResponse)
+ err := c.cc.Invoke(ctx, Admin_GetAdminDlqJob_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *adminClient) RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(RetryAdminDlqJobResponse)
+ err := c.cc.Invoke(ctx, Admin_RetryAdminDlqJob_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *adminClient) RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(RemoveAdminDlqJobResponse)
+ err := c.cc.Invoke(ctx, Admin_RemoveAdminDlqJob_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
func (c *adminClient) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAdminAgentsResponse)
@@ -621,6 +669,10 @@ type AdminServer interface {
CreateAdminJob(context.Context, *CreateAdminJobRequest) (*CreateAdminJobResponse, error)
CancelAdminJob(context.Context, *CancelAdminJobRequest) (*CancelAdminJobResponse, error)
RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error)
+ ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error)
+ GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error)
+ RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error)
+ RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error)
ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error)
RestartAdminAgent(context.Context, *RestartAdminAgentRequest) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(context.Context, *UpdateAdminAgentRequest) (*AdminAgentCommandResponse, error)
@@ -760,6 +812,18 @@ func (UnimplementedAdminServer) CancelAdminJob(context.Context, *CancelAdminJobR
func (UnimplementedAdminServer) RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryAdminJob not implemented")
}
+func (UnimplementedAdminServer) ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ListAdminDlqJobs not implemented")
+}
+func (UnimplementedAdminServer) GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method GetAdminDlqJob not implemented")
+}
+func (UnimplementedAdminServer) RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method RetryAdminDlqJob not implemented")
+}
+func (UnimplementedAdminServer) RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method RemoveAdminDlqJob not implemented")
+}
func (UnimplementedAdminServer) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminAgents not implemented")
}
@@ -1546,6 +1610,78 @@ func _Admin_RetryAdminJob_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
+func _Admin_ListAdminDlqJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListAdminDlqJobsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AdminServer).ListAdminDlqJobs(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Admin_ListAdminDlqJobs_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AdminServer).ListAdminDlqJobs(ctx, req.(*ListAdminDlqJobsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Admin_GetAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetAdminDlqJobRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AdminServer).GetAdminDlqJob(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Admin_GetAdminDlqJob_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AdminServer).GetAdminDlqJob(ctx, req.(*GetAdminDlqJobRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Admin_RetryAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RetryAdminDlqJobRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AdminServer).RetryAdminDlqJob(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Admin_RetryAdminDlqJob_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AdminServer).RetryAdminDlqJob(ctx, req.(*RetryAdminDlqJobRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Admin_RemoveAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RemoveAdminDlqJobRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AdminServer).RemoveAdminDlqJob(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Admin_RemoveAdminDlqJob_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AdminServer).RemoveAdminDlqJob(ctx, req.(*RemoveAdminDlqJobRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
func _Admin_ListAdminAgents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAdminAgentsRequest)
if err := dec(in); err != nil {
@@ -1775,6 +1911,22 @@ var Admin_ServiceDesc = grpc.ServiceDesc{
MethodName: "RetryAdminJob",
Handler: _Admin_RetryAdminJob_Handler,
},
+ {
+ MethodName: "ListAdminDlqJobs",
+ Handler: _Admin_ListAdminDlqJobs_Handler,
+ },
+ {
+ MethodName: "GetAdminDlqJob",
+ Handler: _Admin_GetAdminDlqJob_Handler,
+ },
+ {
+ MethodName: "RetryAdminDlqJob",
+ Handler: _Admin_RetryAdminDlqJob_Handler,
+ },
+ {
+ MethodName: "RemoveAdminDlqJob",
+ Handler: _Admin_RemoveAdminDlqJob_Handler,
+ },
{
MethodName: "ListAdminAgents",
Handler: _Admin_ListAdminAgents_Handler,
diff --git a/internal/api/proto/app/v1/common.pb.go b/internal/api/proto/app/v1/common.pb.go
index b14c454..5f665db 100644
--- a/internal/api/proto/app/v1/common.pb.go
+++ b/internal/api/proto/app/v1/common.pb.go
@@ -3442,6 +3442,74 @@ func (x *AdminAgent) GetUpdatedAt() *timestamppb.Timestamp {
return nil
}
+type AdminDlqEntry struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
+ FailureTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=failure_time,json=failureTime,proto3" json:"failure_time,omitempty"`
+ Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"`
+ RetryCount int32 `protobuf:"varint,4,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AdminDlqEntry) Reset() {
+ *x = AdminDlqEntry{}
+ mi := &file_app_v1_common_proto_msgTypes[27]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AdminDlqEntry) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AdminDlqEntry) ProtoMessage() {}
+
+func (x *AdminDlqEntry) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_common_proto_msgTypes[27]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AdminDlqEntry.ProtoReflect.Descriptor instead.
+func (*AdminDlqEntry) Descriptor() ([]byte, []int) {
+ return file_app_v1_common_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *AdminDlqEntry) GetJob() *AdminJob {
+ if x != nil {
+ return x.Job
+ }
+ return nil
+}
+
+func (x *AdminDlqEntry) GetFailureTime() *timestamppb.Timestamp {
+ if x != nil {
+ return x.FailureTime
+ }
+ return nil
+}
+
+func (x *AdminDlqEntry) GetReason() string {
+ if x != nil {
+ return x.Reason
+ }
+ return ""
+}
+
+func (x *AdminDlqEntry) GetRetryCount() int32 {
+ if x != nil {
+ return x.RetryCount
+ }
+ return 0
+}
+
var File_app_v1_common_proto protoreflect.FileDescriptor
const file_app_v1_common_proto_rawDesc = "" +
@@ -3954,7 +4022,13 @@ const file_app_v1_common_proto_rawDesc = "" +
"\n" +
"created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
- "updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAtB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
+ "updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\xb2\x01\n" +
+ "\rAdminDlqEntry\x12)\n" +
+ "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\x12=\n" +
+ "\ffailure_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\vfailureTime\x12\x16\n" +
+ "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1f\n" +
+ "\vretry_count\x18\x04 \x01(\x05R\n" +
+ "retryCountB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_common_proto_rawDescOnce sync.Once
@@ -3968,7 +4042,7 @@ func file_app_v1_common_proto_rawDescGZIP() []byte {
return file_app_v1_common_proto_rawDescData
}
-var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
+var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_app_v1_common_proto_goTypes = []any{
(*MessageResponse)(nil), // 0: stream.app.v1.MessageResponse
(*User)(nil), // 1: stream.app.v1.User
@@ -3997,61 +4071,64 @@ var file_app_v1_common_proto_goTypes = []any{
(*AdminPopupAd)(nil), // 24: stream.app.v1.AdminPopupAd
(*AdminJob)(nil), // 25: stream.app.v1.AdminJob
(*AdminAgent)(nil), // 26: stream.app.v1.AdminAgent
- (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
+ (*AdminDlqEntry)(nil), // 27: stream.app.v1.AdminDlqEntry
+ (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
}
var file_app_v1_common_proto_depIdxs = []int32{
- 27, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp
- 27, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp
- 27, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp
- 27, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp
- 27, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp
- 27, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp
- 27, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp
- 27, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp
- 27, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp
- 27, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp
- 27, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp
- 27, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp
- 27, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp
- 27, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp
- 27, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp
- 27, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp
- 27, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp
- 27, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp
- 27, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp
+ 28, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp
+ 28, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp
+ 28, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp
+ 28, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp
+ 28, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp
17, // 29: stream.app.v1.AdminUserReferralInfo.referrer:type_name -> stream.app.v1.ReferralUserSummary
- 27, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp
+ 28, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp
16, // 31: stream.app.v1.AdminUserDetail.user:type_name -> stream.app.v1.AdminUser
11, // 32: stream.app.v1.AdminUserDetail.subscription:type_name -> stream.app.v1.PlanSubscription
18, // 33: stream.app.v1.AdminUserDetail.referral:type_name -> stream.app.v1.AdminUserReferralInfo
- 27, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp
- 27, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp
- 27, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp
- 27, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp
- 27, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp
- 27, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp
- 27, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp
- 27, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp
- 27, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp
- 47, // [47:47] is the sub-list for method output_type
- 47, // [47:47] is the sub-list for method input_type
- 47, // [47:47] is the sub-list for extension type_name
- 47, // [47:47] is the sub-list for extension extendee
- 0, // [0:47] is the sub-list for field type_name
+ 28, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp
+ 28, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp
+ 28, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp
+ 25, // 47: stream.app.v1.AdminDlqEntry.job:type_name -> stream.app.v1.AdminJob
+ 28, // 48: stream.app.v1.AdminDlqEntry.failure_time:type_name -> google.protobuf.Timestamp
+ 49, // [49:49] is the sub-list for method output_type
+ 49, // [49:49] is the sub-list for method input_type
+ 49, // [49:49] is the sub-list for extension type_name
+ 49, // [49:49] is the sub-list for extension extendee
+ 0, // [0:49] is the sub-list for field type_name
}
func init() { file_app_v1_common_proto_init() }
@@ -4085,7 +4162,7 @@ func file_app_v1_common_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_common_proto_rawDesc), len(file_app_v1_common_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 27,
+ NumMessages: 28,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/internal/api/proto/app/v1/video_metadata.pb.go b/internal/api/proto/app/v1/video_metadata.pb.go
new file mode 100644
index 0000000..b18e164
--- /dev/null
+++ b/internal/api/proto/app/v1/video_metadata.pb.go
@@ -0,0 +1,221 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc (unknown)
+// source: app/v1/video_metadata.proto
+
+package appv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GetVideoMetadataRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ VideoId string `protobuf:"bytes,1,opt,name=video_id,json=videoId,proto3" json:"video_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetVideoMetadataRequest) Reset() {
+ *x = GetVideoMetadataRequest{}
+ mi := &file_app_v1_video_metadata_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetVideoMetadataRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetVideoMetadataRequest) ProtoMessage() {}
+
+func (x *GetVideoMetadataRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_video_metadata_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetVideoMetadataRequest.ProtoReflect.Descriptor instead.
+func (*GetVideoMetadataRequest) Descriptor() ([]byte, []int) {
+ return file_app_v1_video_metadata_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *GetVideoMetadataRequest) GetVideoId() string {
+ if x != nil {
+ return x.VideoId
+ }
+ return ""
+}
+
+type GetVideoMetadataResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Video *Video `protobuf:"bytes,1,opt,name=video,proto3" json:"video,omitempty"`
+ DefaultPlayerConfig *PlayerConfig `protobuf:"bytes,2,opt,name=default_player_config,json=defaultPlayerConfig,proto3" json:"default_player_config,omitempty"`
+ AdTemplate *AdTemplate `protobuf:"bytes,3,opt,name=ad_template,json=adTemplate,proto3" json:"ad_template,omitempty"`
+ ActivePopupAd *PopupAd `protobuf:"bytes,4,opt,name=active_popup_ad,json=activePopupAd,proto3" json:"active_popup_ad,omitempty"`
+ Domains []*Domain `protobuf:"bytes,5,rep,name=domains,proto3" json:"domains,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetVideoMetadataResponse) Reset() {
+ *x = GetVideoMetadataResponse{}
+ mi := &file_app_v1_video_metadata_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetVideoMetadataResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetVideoMetadataResponse) ProtoMessage() {}
+
+func (x *GetVideoMetadataResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_app_v1_video_metadata_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetVideoMetadataResponse.ProtoReflect.Descriptor instead.
+func (*GetVideoMetadataResponse) Descriptor() ([]byte, []int) {
+ return file_app_v1_video_metadata_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GetVideoMetadataResponse) GetVideo() *Video {
+ if x != nil {
+ return x.Video
+ }
+ return nil
+}
+
+func (x *GetVideoMetadataResponse) GetDefaultPlayerConfig() *PlayerConfig {
+ if x != nil {
+ return x.DefaultPlayerConfig
+ }
+ return nil
+}
+
+func (x *GetVideoMetadataResponse) GetAdTemplate() *AdTemplate {
+ if x != nil {
+ return x.AdTemplate
+ }
+ return nil
+}
+
+func (x *GetVideoMetadataResponse) GetActivePopupAd() *PopupAd {
+ if x != nil {
+ return x.ActivePopupAd
+ }
+ return nil
+}
+
+func (x *GetVideoMetadataResponse) GetDomains() []*Domain {
+ if x != nil {
+ return x.Domains
+ }
+ return nil
+}
+
+var File_app_v1_video_metadata_proto protoreflect.FileDescriptor
+
+const file_app_v1_video_metadata_proto_rawDesc = "" +
+ "\n" +
+ "\x1bapp/v1/video_metadata.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"4\n" +
+ "\x17GetVideoMetadataRequest\x12\x19\n" +
+ "\bvideo_id\x18\x01 \x01(\tR\avideoId\"\xc4\x02\n" +
+ "\x18GetVideoMetadataResponse\x12*\n" +
+ "\x05video\x18\x01 \x01(\v2\x14.stream.app.v1.VideoR\x05video\x12O\n" +
+ "\x15default_player_config\x18\x02 \x01(\v2\x1b.stream.app.v1.PlayerConfigR\x13defaultPlayerConfig\x12:\n" +
+ "\vad_template\x18\x03 \x01(\v2\x19.stream.app.v1.AdTemplateR\n" +
+ "adTemplate\x12>\n" +
+ "\x0factive_popup_ad\x18\x04 \x01(\v2\x16.stream.app.v1.PopupAdR\ractivePopupAd\x12/\n" +
+ "\adomains\x18\x05 \x03(\v2\x15.stream.app.v1.DomainR\adomains2t\n" +
+ "\rVideoMetadata\x12c\n" +
+ "\x10GetVideoMetadata\x12&.stream.app.v1.GetVideoMetadataRequest\x1a'.stream.app.v1.GetVideoMetadataResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
+
+var (
+ file_app_v1_video_metadata_proto_rawDescOnce sync.Once
+ file_app_v1_video_metadata_proto_rawDescData []byte
+)
+
+func file_app_v1_video_metadata_proto_rawDescGZIP() []byte {
+ file_app_v1_video_metadata_proto_rawDescOnce.Do(func() {
+ file_app_v1_video_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_video_metadata_proto_rawDesc), len(file_app_v1_video_metadata_proto_rawDesc)))
+ })
+ return file_app_v1_video_metadata_proto_rawDescData
+}
+
+var file_app_v1_video_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_app_v1_video_metadata_proto_goTypes = []any{
+ (*GetVideoMetadataRequest)(nil), // 0: stream.app.v1.GetVideoMetadataRequest
+ (*GetVideoMetadataResponse)(nil), // 1: stream.app.v1.GetVideoMetadataResponse
+ (*Video)(nil), // 2: stream.app.v1.Video
+ (*PlayerConfig)(nil), // 3: stream.app.v1.PlayerConfig
+ (*AdTemplate)(nil), // 4: stream.app.v1.AdTemplate
+ (*PopupAd)(nil), // 5: stream.app.v1.PopupAd
+ (*Domain)(nil), // 6: stream.app.v1.Domain
+}
+var file_app_v1_video_metadata_proto_depIdxs = []int32{
+ 2, // 0: stream.app.v1.GetVideoMetadataResponse.video:type_name -> stream.app.v1.Video
+ 3, // 1: stream.app.v1.GetVideoMetadataResponse.default_player_config:type_name -> stream.app.v1.PlayerConfig
+ 4, // 2: stream.app.v1.GetVideoMetadataResponse.ad_template:type_name -> stream.app.v1.AdTemplate
+ 5, // 3: stream.app.v1.GetVideoMetadataResponse.active_popup_ad:type_name -> stream.app.v1.PopupAd
+ 6, // 4: stream.app.v1.GetVideoMetadataResponse.domains:type_name -> stream.app.v1.Domain
+ 0, // 5: stream.app.v1.VideoMetadata.GetVideoMetadata:input_type -> stream.app.v1.GetVideoMetadataRequest
+ 1, // 6: stream.app.v1.VideoMetadata.GetVideoMetadata:output_type -> stream.app.v1.GetVideoMetadataResponse
+ 6, // [6:7] is the sub-list for method output_type
+ 5, // [5:6] is the sub-list for method input_type
+ 5, // [5:5] is the sub-list for extension type_name
+ 5, // [5:5] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_app_v1_video_metadata_proto_init() }
+func file_app_v1_video_metadata_proto_init() {
+ if File_app_v1_video_metadata_proto != nil {
+ return
+ }
+ file_app_v1_common_proto_init()
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_video_metadata_proto_rawDesc), len(file_app_v1_video_metadata_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_app_v1_video_metadata_proto_goTypes,
+ DependencyIndexes: file_app_v1_video_metadata_proto_depIdxs,
+ MessageInfos: file_app_v1_video_metadata_proto_msgTypes,
+ }.Build()
+ File_app_v1_video_metadata_proto = out.File
+ file_app_v1_video_metadata_proto_goTypes = nil
+ file_app_v1_video_metadata_proto_depIdxs = nil
+}
diff --git a/internal/api/proto/app/v1/video_metadata_grpc.pb.go b/internal/api/proto/app/v1/video_metadata_grpc.pb.go
new file mode 100644
index 0000000..4e59f08
--- /dev/null
+++ b/internal/api/proto/app/v1/video_metadata_grpc.pb.go
@@ -0,0 +1,121 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.6.1
+// - protoc (unknown)
+// source: app/v1/video_metadata.proto
+
+package appv1
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ VideoMetadata_GetVideoMetadata_FullMethodName = "/stream.app.v1.VideoMetadata/GetVideoMetadata"
+)
+
+// VideoMetadataClient is the client API for VideoMetadata service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type VideoMetadataClient interface {
+ GetVideoMetadata(ctx context.Context, in *GetVideoMetadataRequest, opts ...grpc.CallOption) (*GetVideoMetadataResponse, error)
+}
+
+type videoMetadataClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewVideoMetadataClient(cc grpc.ClientConnInterface) VideoMetadataClient {
+ return &videoMetadataClient{cc}
+}
+
+func (c *videoMetadataClient) GetVideoMetadata(ctx context.Context, in *GetVideoMetadataRequest, opts ...grpc.CallOption) (*GetVideoMetadataResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetVideoMetadataResponse)
+ err := c.cc.Invoke(ctx, VideoMetadata_GetVideoMetadata_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// VideoMetadataServer is the server API for VideoMetadata service.
+// All implementations must embed UnimplementedVideoMetadataServer
+// for forward compatibility.
+type VideoMetadataServer interface {
+ GetVideoMetadata(context.Context, *GetVideoMetadataRequest) (*GetVideoMetadataResponse, error)
+ mustEmbedUnimplementedVideoMetadataServer()
+}
+
+// UnimplementedVideoMetadataServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedVideoMetadataServer struct{}
+
+func (UnimplementedVideoMetadataServer) GetVideoMetadata(context.Context, *GetVideoMetadataRequest) (*GetVideoMetadataResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method GetVideoMetadata not implemented")
+}
+func (UnimplementedVideoMetadataServer) mustEmbedUnimplementedVideoMetadataServer() {}
+func (UnimplementedVideoMetadataServer) testEmbeddedByValue() {}
+
+// UnsafeVideoMetadataServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to VideoMetadataServer will
+// result in compilation errors.
+type UnsafeVideoMetadataServer interface {
+ mustEmbedUnimplementedVideoMetadataServer()
+}
+
+func RegisterVideoMetadataServer(s grpc.ServiceRegistrar, srv VideoMetadataServer) {
+ // If the following call panics, it indicates UnimplementedVideoMetadataServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&VideoMetadata_ServiceDesc, srv)
+}
+
+func _VideoMetadata_GetVideoMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetVideoMetadataRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(VideoMetadataServer).GetVideoMetadata(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: VideoMetadata_GetVideoMetadata_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(VideoMetadataServer).GetVideoMetadata(ctx, req.(*GetVideoMetadataRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// VideoMetadata_ServiceDesc is the grpc.ServiceDesc for VideoMetadata service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var VideoMetadata_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "stream.app.v1.VideoMetadata",
+ HandlerType: (*VideoMetadataServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "GetVideoMetadata",
+ Handler: _VideoMetadata_GetVideoMetadata_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "app/v1/video_metadata.proto",
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 7f784bf..18b2112 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -11,7 +11,6 @@ type Config struct {
Database DatabaseConfig
Redis RedisConfig
Google GoogleConfig
- Frontend FrontendConfig
Email EmailConfig
AWS AWSConfig
Render RenderConfig
@@ -25,7 +24,6 @@ type ServerConfig struct {
}
type RenderConfig struct {
- AgentSecret string `mapstructure:"agent_secret"`
EnableMetrics bool `mapstructure:"enable_metrics"`
EnableTracing bool `mapstructure:"enable_tracing"`
OTLPEndpoint string `mapstructure:"otlp_endpoint"`
@@ -53,11 +51,6 @@ type GoogleConfig struct {
StateTTLMinute int `mapstructure:"state_ttl_minutes"`
}
-type FrontendConfig struct {
- BaseURL string `mapstructure:"base_url"`
- GoogleAuthFinalizePath string `mapstructure:"google_auth_finalize_path"`
-}
-
type EmailConfig struct {
From string
// Add SMTP settings here later
@@ -83,7 +76,6 @@ func LoadConfig() (*Config, error) {
v.SetDefault("render.enable_tracing", false)
v.SetDefault("render.service_name", "stream-api-render")
v.SetDefault("google.state_ttl_minutes", 10)
- v.SetDefault("frontend.google_auth_finalize_path", "/auth/google/finalize")
v.SetDefault("internal.marker", "")
// Environment variable settings
diff --git a/internal/dto/job.go b/internal/dto/job.go
index 535668b..03949d5 100644
--- a/internal/dto/job.go
+++ b/internal/dto/job.go
@@ -44,3 +44,10 @@ type JobConfigEnvelope struct {
VideoID string `json:"video_id,omitempty"`
TimeLimit int64 `json:"time_limit,omitempty"`
}
+
+type DLQEntry struct {
+ Job *model.Job `json:"job,omitempty"`
+ FailureTime int64 `json:"failure_time_unix,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ RetryCount int64 `json:"retry_count,omitempty"`
+}
diff --git a/internal/repository/job_repository.go b/internal/repository/job_repository.go
index 88208db..a641e27 100644
--- a/internal/repository/job_repository.go
+++ b/internal/repository/job_repository.go
@@ -100,3 +100,82 @@ func (r *jobRepository) GetLatestByVideoID(ctx context.Context, videoID string)
}
return &job, nil
}
+
+func (r *jobRepository) AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error) {
+ result, err := query.Job.WithContext(ctx).
+ Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("pending"), query.Job.Cancelled.Is(false)).
+ Updates(map[string]any{"status": "running", "agent_id": agentID, "updated_at": now})
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
+
+func (r *jobRepository) MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error) {
+ jobID = strings.TrimSpace(jobID)
+ updates := map[string]any{"status": toStatus, "updated_at": now}
+ if clearAgent {
+ updates["agent_id"] = nil
+ }
+ q := query.Job.WithContext(ctx).Where(query.Job.ID.Eq(jobID), query.Job.Status.In(fromStatuses...))
+ result, err := q.Updates(updates)
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
+
+func (r *jobRepository) CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error) {
+ result, err := query.Job.WithContext(ctx).
+ Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("pending", "running")).
+ Updates(map[string]any{"status": "cancelled", "cancelled": true, "updated_at": now})
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
+
+func (r *jobRepository) RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error) {
+ updates := map[string]any{"status": "pending", "cancelled": false, "progress": 0, "agent_id": nil, "retry_count": retryCount, "updated_at": now}
+ if logs != nil {
+ updates["logs"] = *logs
+ }
+ result, err := query.Job.WithContext(ctx).
+ Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending", "failure", "cancelled")).
+ Updates(updates)
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
+
+func (r *jobRepository) MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error) {
+ updates := map[string]any{"status": "failure", "agent_id": nil, "updated_at": now}
+ if logs != nil {
+ updates["logs"] = *logs
+ }
+ result, err := query.Job.WithContext(ctx).
+ Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending")).
+ Updates(updates)
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
+
+func (r *jobRepository) UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error) {
+ updates := map[string]any{"updated_at": now}
+ if progress != nil {
+ updates["progress"] = *progress
+ }
+ if logs != nil {
+ updates["logs"] = *logs
+ }
+ result, err := query.Job.WithContext(ctx).
+ Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("running")).
+ Updates(updates)
+ if err != nil {
+ return false, err
+ }
+ return result.RowsAffected > 0, nil
+}
diff --git a/internal/service/__test__/service_admin_jobs_agents_test.go b/internal/service/__test__/service_admin_jobs_agents_test.go
index 5cef158..3222a81 100644
--- a/internal/service/__test__/service_admin_jobs_agents_test.go
+++ b/internal/service/__test__/service_admin_jobs_agents_test.go
@@ -1,15 +1,17 @@
package service
import (
+ "context"
+ "fmt"
"testing"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"gorm.io/gorm"
+ redisadapter "stream.api/internal/adapters/redis"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
- runtimeservices "stream.api/internal/service/runtime/services"
renderworkflow "stream.api/internal/workflow/render"
)
@@ -18,7 +20,7 @@ func TestListAdminJobsCursorPagination(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
- services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
@@ -67,7 +69,7 @@ func TestListAdminJobsInvalidCursor(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
- services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
conn, cleanup := newTestGRPCServer(t, services)
@@ -86,7 +88,7 @@ func TestListAdminJobsCursorRejectsAgentMismatch(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
- services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 11, 0, 0, 0, time.UTC)
@@ -192,4 +194,212 @@ func assertAdminJobIDs(t *testing.T, jobs []*appv1.AdminJob, want []string) {
}
}
-func ptrTime(v time.Time) *time.Time { return &v }
+type fakeDLQ struct {
+ entries map[string]*redisadapter.DLQEntry
+ order []string
+ listErr error
+ getErr error
+ removeErr error
+ retryErr error
+}
+
+func newFakeDLQ(entries ...*redisadapter.DLQEntry) *fakeDLQ {
+ f := &fakeDLQ{entries: map[string]*redisadapter.DLQEntry{}, order: []string{}}
+ for _, entry := range entries {
+ if entry == nil || entry.Job == nil {
+ continue
+ }
+ f.entries[entry.Job.ID] = entry
+ f.order = append(f.order, entry.Job.ID)
+ }
+ return f
+}
+
+func (f *fakeDLQ) Add(_ context.Context, job *model.Job, reason string) error {
+ if f.entries == nil {
+ f.entries = map[string]*redisadapter.DLQEntry{}
+ }
+ entry := &redisadapter.DLQEntry{Job: job, FailureTime: time.Now().UTC(), Reason: reason}
+ f.entries[job.ID] = entry
+ f.order = append(f.order, job.ID)
+ return nil
+}
+
+func (f *fakeDLQ) Get(_ context.Context, jobID string) (*redisadapter.DLQEntry, error) {
+ if f.getErr != nil {
+ return nil, f.getErr
+ }
+ entry, ok := f.entries[jobID]
+ if !ok {
+ return nil, fmt.Errorf("job not found in DLQ")
+ }
+ return entry, nil
+}
+
+func (f *fakeDLQ) List(_ context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error) {
+ if f.listErr != nil {
+ return nil, f.listErr
+ }
+ if offset < 0 {
+ offset = 0
+ }
+ if limit <= 0 {
+ limit = int64(len(f.order))
+ }
+ items := make([]*redisadapter.DLQEntry, 0)
+ for i := offset; i < int64(len(f.order)) && int64(len(items)) < limit; i++ {
+ id := f.order[i]
+ if entry, ok := f.entries[id]; ok {
+ items = append(items, entry)
+ }
+ }
+ return items, nil
+}
+
+func (f *fakeDLQ) Count(_ context.Context) (int64, error) {
+ return int64(len(f.entries)), nil
+}
+
+func (f *fakeDLQ) Remove(_ context.Context, jobID string) error {
+ if f.removeErr != nil {
+ return f.removeErr
+ }
+ if _, ok := f.entries[jobID]; !ok {
+ return fmt.Errorf("job not found in DLQ")
+ }
+ delete(f.entries, jobID)
+ filtered := make([]string, 0, len(f.order))
+ for _, id := range f.order {
+ if id != jobID {
+ filtered = append(filtered, id)
+ }
+ }
+ f.order = filtered
+ return nil
+}
+
+func (f *fakeDLQ) Retry(ctx context.Context, jobID string) (*model.Job, error) {
+ if f.retryErr != nil {
+ return nil, f.retryErr
+ }
+ entry, err := f.Get(ctx, jobID)
+ if err != nil {
+ return nil, err
+ }
+ if err := f.Remove(ctx, jobID); err != nil {
+ return nil, err
+ }
+ return entry.Job, nil
+}
+
+type fakeQueue struct {
+ enqueueErr error
+}
+
+func (f *fakeQueue) Enqueue(_ context.Context, _ *model.Job) error { return f.enqueueErr }
+func (f *fakeQueue) Dequeue(_ context.Context) (*model.Job, error) { return nil, nil }
+
+func TestAdminDlqJobs(t *testing.T) {
+ t.Run("list happy path", func(t *testing.T) {
+ db := newTestDB(t)
+ ensureTestJobsTable(t, db)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ job1 := seedTestJob(t, db, model.Job{ID: "job-dlq-1", CreatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC())})
+ job2 := seedTestJob(t, db, model.Job{ID: "job-dlq-2", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
+ services := newTestAppServices(t, db)
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
+ &redisadapter.DLQEntry{Job: &job1, FailureTime: time.Now().Add(-30 * time.Minute).UTC(), Reason: "lease_expired", RetryCount: 2},
+ &redisadapter.DLQEntry{Job: &job2, FailureTime: time.Now().Add(-10 * time.Minute).UTC(), Reason: "invalid_config", RetryCount: 3},
+ )))
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newAdminClient(conn)
+ resp, err := client.ListAdminDlqJobs(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.ListAdminDlqJobsRequest{Offset: 0, Limit: 10})
+ if err != nil {
+ t.Fatalf("ListAdminDlqJobs error = %v", err)
+ }
+ if resp.GetTotal() != 2 {
+ t.Fatalf("total = %d, want 2", resp.GetTotal())
+ }
+ if len(resp.GetItems()) != 2 {
+ t.Fatalf("items len = %d, want 2", len(resp.GetItems()))
+ }
+ if resp.GetItems()[0].GetJob().GetId() != "job-dlq-1" {
+ t.Fatalf("first job id = %q", resp.GetItems()[0].GetJob().GetId())
+ }
+ if resp.GetItems()[0].GetReason() != "lease_expired" {
+ t.Fatalf("first reason = %q", resp.GetItems()[0].GetReason())
+ }
+ })
+
+ t.Run("get not found", func(t *testing.T) {
+ db := newTestDB(t)
+ ensureTestJobsTable(t, db)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ services := newTestAppServices(t, db)
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newAdminClient(conn)
+ _, err := client.GetAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.GetAdminDlqJobRequest{Id: "missing"})
+ assertGRPCCode(t, err, codes.NotFound)
+ })
+
+ t.Run("retry happy path", func(t *testing.T) {
+ db := newTestDB(t)
+ ensureTestJobsTable(t, db)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ job := seedTestJob(t, db, model.Job{ID: "job-dlq-retry", Status: ptrString("failure"), CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
+ services := newTestAppServices(t, db)
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
+ &redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "lease_expired", RetryCount: 1},
+ )))
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newAdminClient(conn)
+ resp, err := client.RetryAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RetryAdminDlqJobRequest{Id: job.ID})
+ if err != nil {
+ t.Fatalf("RetryAdminDlqJob error = %v", err)
+ }
+ if resp.GetJob().GetId() != job.ID {
+ t.Fatalf("job id = %q, want %q", resp.GetJob().GetId(), job.ID)
+ }
+ if resp.GetJob().GetStatus() != "pending" {
+ t.Fatalf("job status = %q, want pending", resp.GetJob().GetStatus())
+ }
+ })
+
+ t.Run("remove happy path", func(t *testing.T) {
+ db := newTestDB(t)
+ ensureTestJobsTable(t, db)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ job := seedTestJob(t, db, model.Job{ID: "job-dlq-remove", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
+ services := newTestAppServices(t, db)
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
+ &redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "invalid_config", RetryCount: 3},
+ )))
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newAdminClient(conn)
+ resp, err := client.RemoveAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RemoveAdminDlqJobRequest{Id: job.ID})
+ if err != nil {
+ t.Fatalf("RemoveAdminDlqJob error = %v", err)
+ }
+ if resp.GetStatus() != "removed" {
+ t.Fatalf("status = %q, want removed", resp.GetStatus())
+ }
+ })
+
+ t.Run("list permission denied", func(t *testing.T) {
+ db := newTestDB(t)
+ ensureTestJobsTable(t, db)
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: ptrString("USER")})
+ services := newTestAppServices(t, db)
+ services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newAdminClient(conn)
+ _, err := client.ListAdminDlqJobs(testActorOutgoingContext(user.ID, "USER"), &appv1.ListAdminDlqJobsRequest{})
+ assertGRPCCode(t, err, codes.PermissionDenied)
+ })
+}
diff --git a/internal/service/__test__/service_video_metadata_test.go b/internal/service/__test__/service_video_metadata_test.go
new file mode 100644
index 0000000..ffbf35e
--- /dev/null
+++ b/internal/service/__test__/service_video_metadata_test.go
@@ -0,0 +1,245 @@
+package service
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/metadata"
+ appv1 "stream.api/internal/api/proto/app/v1"
+ "stream.api/internal/database/model"
+ "stream.api/internal/middleware"
+)
+
+func testInternalMetadataContext() context.Context {
+ return metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
+ middleware.ActorMarkerMetadataKey, testTrustedMarker,
+ ))
+}
+
+func testInvalidInternalMetadataContext() context.Context {
+ return metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
+ middleware.ActorMarkerMetadataKey, "wrong-marker",
+ ))
+}
+
+func TestGetVideoMetadata(t *testing.T) {
+ t.Run("happy path with owner defaults", func(t *testing.T) {
+ db := newTestDB(t)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: nil}
+ if err := db.Create(&video).Error; err != nil {
+ t.Fatalf("create video: %v", err)
+ }
+ if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error; err != nil {
+ t.Fatalf("create domain: %v", err)
+ }
+ if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
+ t.Fatalf("create ad template: %v", err)
+ }
+ if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create popup ad: %v", err)
+ }
+ if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create player config: %v", err)
+ }
+ services := newTestAppServices(t, db)
+ _ = admin
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ if err != nil {
+ t.Fatalf("GetVideoMetadata error = %v", err)
+ }
+ if resp.GetVideo().GetId() != video.ID {
+ t.Fatalf("video id = %q, want %q", resp.GetVideo().GetId(), video.ID)
+ }
+ if resp.GetAdTemplate().GetName() != "default-ad" {
+ t.Fatalf("ad template name = %q", resp.GetAdTemplate().GetName())
+ }
+ if resp.GetActivePopupAd().GetLabel() != "promo" {
+ t.Fatalf("popup label = %q", resp.GetActivePopupAd().GetLabel())
+ }
+ if resp.GetDefaultPlayerConfig().GetName() != "default-player" {
+ t.Fatalf("player config name = %q", resp.GetDefaultPlayerConfig().GetName())
+ }
+ if len(resp.GetDomains()) != 1 {
+ t.Fatalf("domains len = %d, want 1", len(resp.GetDomains()))
+ }
+ })
+
+ t.Run("missing ad template returns failed precondition", func(t *testing.T) {
+ db := newTestDB(t)
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
+ _ = db.Create(&video).Error
+ _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
+ _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ assertGRPCCode(t, err, codes.FailedPrecondition)
+ })
+
+ t.Run("missing popup ad returns failed precondition", func(t *testing.T) {
+ db := newTestDB(t)
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
+ _ = db.Create(&video).Error
+ _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
+ _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
+ _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ assertGRPCCode(t, err, codes.FailedPrecondition)
+ })
+
+ t.Run("missing player config returns failed precondition", func(t *testing.T) {
+ db := newTestDB(t)
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
+ _ = db.Create(&video).Error
+ _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
+ _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
+ _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ assertGRPCCode(t, err, codes.FailedPrecondition)
+ })
+
+ t.Run("missing domains returns failed precondition", func(t *testing.T) {
+ db := newTestDB(t)
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
+ _ = db.Create(&video).Error
+ _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
+ _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ assertGRPCCode(t, err, codes.FailedPrecondition)
+ })
+
+ t.Run("free user falls back to system admin config", func(t *testing.T) {
+ db := newTestDB(t)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")})
+ now := time.Now().UTC()
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
+ if err := db.Create(&video).Error; err != nil {
+ t.Fatalf("create video: %v", err)
+ }
+ if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil {
+ t.Fatalf("create domain: %v", err)
+ }
+ if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
+ t.Fatalf("create system ad template: %v", err)
+ }
+ if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create system popup ad: %v", err)
+ }
+ if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create system player config: %v", err)
+ }
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ if err != nil {
+ t.Fatalf("GetVideoMetadata error = %v", err)
+ }
+ if resp.GetAdTemplate().GetName() != "system-ad" {
+ t.Fatalf("ad template = %q, want system-ad", resp.GetAdTemplate().GetName())
+ }
+ if resp.GetActivePopupAd().GetLabel() != "system-popup" {
+ t.Fatalf("popup label = %q, want system-popup", resp.GetActivePopupAd().GetLabel())
+ }
+ if resp.GetDefaultPlayerConfig().GetName() != "system-player" {
+ t.Fatalf("player config = %q, want system-player", resp.GetDefaultPlayerConfig().GetName())
+ }
+ if len(resp.GetDomains()) != 1 || resp.GetDomains()[0].GetName() != "free.example.com" {
+ t.Fatalf("domains = %#v, want owner domains", resp.GetDomains())
+ }
+ })
+
+ t.Run("video ad id takes precedence over fallback ad template", func(t *testing.T) {
+ db := newTestDB(t)
+ admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
+ user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")})
+ now := time.Now().UTC()
+ videoAd := model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "video-ad", VastTagURL: "https://ads.example.com/video", IsDefault: false, IsActive: ptrBool(true)}
+ if err := db.Create(&videoAd).Error; err != nil {
+ t.Fatalf("create video ad template: %v", err)
+ }
+ video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: &videoAd.ID}
+ if err := db.Create(&video).Error; err != nil {
+ t.Fatalf("create video: %v", err)
+ }
+ if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil {
+ t.Fatalf("create domain: %v", err)
+ }
+ if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
+ t.Fatalf("create system ad template: %v", err)
+ }
+ if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create system popup ad: %v", err)
+ }
+ if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
+ t.Fatalf("create system player config: %v", err)
+ }
+ services := newTestAppServices(t, db)
+ _ = admin
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
+ if err != nil {
+ t.Fatalf("GetVideoMetadata error = %v", err)
+ }
+ if resp.GetAdTemplate().GetName() != "video-ad" {
+ t.Fatalf("ad template = %q, want video-ad", resp.GetAdTemplate().GetName())
+ }
+ })
+
+ t.Run("invalid marker returns unauthenticated", func(t *testing.T) {
+ db := newTestDB(t)
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(testInvalidInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()})
+ assertGRPCCode(t, err, codes.Unauthenticated)
+ })
+
+ t.Run("missing marker returns unauthenticated", func(t *testing.T) {
+ db := newTestDB(t)
+ services := newTestAppServices(t, db)
+ conn, cleanup := newTestGRPCServer(t, services)
+ defer cleanup()
+ client := newVideoMetadataClient(conn)
+ _, err := client.GetVideoMetadata(context.Background(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()})
+ assertGRPCCode(t, err, codes.Unauthenticated)
+ })
+}
diff --git a/internal/service/__test__/testdb_setup_test.go b/internal/service/__test__/testdb_setup_test.go
index ea8673b..e672e27 100644
--- a/internal/service/__test__/testdb_setup_test.go
+++ b/internal/service/__test__/testdb_setup_test.go
@@ -9,13 +9,13 @@ import (
"time"
"github.com/google/uuid"
+ _ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
- _ "github.com/mattn/go-sqlite3"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
@@ -210,14 +210,11 @@ func newTestDB(t *testing.T) *gorm.DB {
`CREATE TABLE popup_ads (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
- title TEXT NOT NULL,
- image_url TEXT NOT NULL,
- target_url TEXT NOT NULL,
+ type TEXT NOT NULL,
+ label TEXT NOT NULL,
+ value TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
- start_at DATETIME,
- end_at DATETIME,
- priority INTEGER NOT NULL DEFAULT 0,
- close_cooldown_minutes INTEGER NOT NULL DEFAULT 60,
+ max_triggers_per_session INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
version INTEGER NOT NULL DEFAULT 1
@@ -273,6 +270,7 @@ func newTestGRPCServer(t *testing.T, services *appServices) (*grpc.ClientConn, f
PlansServer: services,
PaymentsServer: services,
VideosServer: services,
+ VideoMetadataServer: services,
AdminServer: services,
})
@@ -407,6 +405,10 @@ func newAdminClient(conn *grpc.ClientConn) appv1.AdminClient {
return appv1.NewAdminClient(conn)
}
+func newVideoMetadataClient(conn *grpc.ClientConn) appv1.VideoMetadataClient {
+ return appv1.NewVideoMetadataClient(conn)
+}
+
func ptrTime(v time.Time) *time.Time { return &v }
func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd {
diff --git a/internal/service/admin_helpers.go b/internal/service/admin_helpers.go
index a078267..716c48e 100644
--- a/internal/service/admin_helpers.go
+++ b/internal/service/admin_helpers.go
@@ -65,31 +65,61 @@ func buildAdminJob(job *model.Job) *appv1.AdminJob {
if job == nil {
return nil
}
- agentID := strconv.FormatInt(*job.AgentID, 10)
+ var agentID *string
+ if job.AgentID != nil {
+ value := strconv.FormatInt(*job.AgentID, 10)
+ agentID = &value
+ }
return &appv1.AdminJob{
Id: job.ID,
- Status: string(*job.Status),
- Priority: int32(*job.Priority),
- UserId: *job.UserID,
+ Status: stringValue(job.Status),
+ Priority: int32(int64Value(job.Priority)),
+ UserId: stringValue(job.UserID),
Name: job.ID,
- TimeLimit: *job.TimeLimit,
- InputUrl: *job.InputURL,
- OutputUrl: *job.OutputURL,
- TotalDuration: *job.TotalDuration,
- CurrentTime: *job.CurrentTime,
- Progress: *job.Progress,
- AgentId: &agentID,
- Logs: *job.Logs,
- Config: *job.Config,
- Cancelled: *job.Cancelled,
- RetryCount: int32(*job.RetryCount),
- MaxRetries: int32(*job.MaxRetries),
- CreatedAt: timestamppb.New(*job.CreatedAt),
- UpdatedAt: timestamppb.New(*job.UpdatedAt),
- VideoId: stringPointerOrNil(*job.VideoID),
+ TimeLimit: int64Value(job.TimeLimit),
+ InputUrl: stringValue(job.InputURL),
+ OutputUrl: stringValue(job.OutputURL),
+ TotalDuration: int64Value(job.TotalDuration),
+ CurrentTime: int64Value(job.CurrentTime),
+ Progress: float64Value(job.Progress),
+ AgentId: agentID,
+ Logs: stringValue(job.Logs),
+ Config: stringValue(job.Config),
+ Cancelled: boolValue(job.Cancelled),
+ RetryCount: int32(int64Value(job.RetryCount)),
+ MaxRetries: int32(int64Value(job.MaxRetries)),
+ CreatedAt: timeToProto(job.CreatedAt),
+ UpdatedAt: timeToProto(job.UpdatedAt),
+ VideoId: job.VideoID,
}
}
+func buildAdminDlqEntry(entry *dto.DLQEntry) *appv1.AdminDlqEntry {
+ if entry == nil {
+ return nil
+ }
+ return &appv1.AdminDlqEntry{
+ Job: buildAdminJob(entry.Job),
+ FailureTime: timestamppb.New(time.Unix(entry.FailureTime, 0).UTC()),
+ Reason: entry.Reason,
+ RetryCount: int32(entry.RetryCount),
+ }
+}
+
+func int64Value(value *int64) int64 {
+ if value == nil {
+ return 0
+ }
+ return *value
+}
+
+func float64Value(value *float64) float64 {
+ if value == nil {
+ return 0
+ }
+ return *value
+}
+
func buildAdminAgent(agent *dto.AgentWithStats) *appv1.AdminAgent {
if agent == nil || agent.Agent == nil {
return nil
@@ -526,15 +556,20 @@ func (s *appServices) buildAdminPopupAd(ctx context.Context, item *model.PopupAd
}
payload := &appv1.AdminPopupAd{
- Id: item.ID,
- UserId: item.UserID,
- Type: item.Type,
- Label: item.Label,
- Value: item.Value,
- IsActive: boolValue(item.IsActive),
- MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
- CreatedAt: timeToProto(item.CreatedAt),
- UpdatedAt: timeToProto(item.UpdatedAt),
+ Id: item.ID,
+ UserId: item.UserID,
+ Type: item.Type,
+ Label: item.Label,
+ Value: item.Value,
+ IsActive: boolValue(item.IsActive),
+ MaxTriggersPerSession: func() int32 {
+ if item.MaxTriggersPerSession != nil {
+ return *item.MaxTriggersPerSession
+ }
+ return 0
+ }(),
+ CreatedAt: timeToProto(item.CreatedAt),
+ UpdatedAt: timeToProto(item.UpdatedAt),
}
ownerEmail, err := s.loadAdminUserEmail(ctx, item.UserID)
diff --git a/internal/service/catalog_mapper.go b/internal/service/catalog_mapper.go
index 73fb5a3..9bd7c68 100644
--- a/internal/service/catalog_mapper.go
+++ b/internal/service/catalog_mapper.go
@@ -40,14 +40,19 @@ func toProtoPopupAd(item *model.PopupAd) *appv1.PopupAd {
return nil
}
return &appv1.PopupAd{
- Id: item.ID,
- Type: item.Type,
- Label: item.Label,
- Value: item.Value,
- IsActive: boolValue(item.IsActive),
- MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
- CreatedAt: timeToProto(item.CreatedAt),
- UpdatedAt: timeToProto(item.UpdatedAt),
+ Id: item.ID,
+ Type: item.Type,
+ Label: item.Label,
+ Value: item.Value,
+ IsActive: boolValue(item.IsActive),
+ MaxTriggersPerSession: func() int32 {
+ if item.MaxTriggersPerSession != nil {
+ return *item.MaxTriggersPerSession
+ }
+ return 0
+ }(),
+ CreatedAt: timeToProto(item.CreatedAt),
+ UpdatedAt: timeToProto(item.UpdatedAt),
}
}
diff --git a/internal/service/interface.go b/internal/service/interface.go
index f3daa84..a3aea13 100644
--- a/internal/service/interface.go
+++ b/internal/service/interface.go
@@ -125,6 +125,10 @@ type VideoWorkflow interface {
CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*model.Job, error)
CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, error)
+ ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error)
+ GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error)
+ RetryDLQ(ctx context.Context, id string) (*model.Job, error)
+ RemoveDLQ(ctx context.Context, id string) error
}
type VideoRepository interface {
@@ -199,6 +203,12 @@ type JobRepository interface {
ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error)
Save(ctx context.Context, job *model.Job) error
UpdateVideoStatus(ctx context.Context, videoID string, statusValue string, processingStatus string) error
+ AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error)
+ MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error)
+ CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error)
+ RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error)
+ MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error)
+ UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error)
}
type AgentRuntime interface {
diff --git a/internal/service/notification_events_test.go b/internal/service/notification_events_test.go
index 57017f6..5a89cbf 100644
--- a/internal/service/notification_events_test.go
+++ b/internal/service/notification_events_test.go
@@ -8,9 +8,9 @@ import (
"github.com/google/uuid"
"google.golang.org/grpc/metadata"
- _ "modernc.org/sqlite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
+ _ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
@@ -89,8 +89,8 @@ func notificationTestContext(userID, role string) context.Context {
))
}
-func notificationPtrString(v string) *string { return &v }
-func notificationPtrBool(v bool) *bool { return &v }
+func notificationPtrString(v string) *string { return &v }
+func notificationPtrBool(v bool) *bool { return &v }
func notificationPtrFloat64(v float64) *float64 { return &v }
func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User {
diff --git a/internal/service/referral_helpers.go b/internal/service/referral_helpers.go
index e75a5f3..6e77df1 100644
--- a/internal/service/referral_helpers.go
+++ b/internal/service/referral_helpers.go
@@ -65,12 +65,7 @@ func (s *appServices) buildReferralShareLink(username *string) *string {
return nil
}
path := "/ref/" + url.PathEscape(trimmed)
- base := strings.TrimRight(strings.TrimSpace(s.frontendBaseURL), "/")
- if base == "" {
- return &path
- }
- link := base + path
- return &link
+ return &path
}
func (s *appServices) loadReferralUsersByUsername(ctx context.Context, username string) ([]model.User, error) {
diff --git a/internal/service/register.go b/internal/service/register.go
index d49e555..136278a 100644
--- a/internal/service/register.go
+++ b/internal/service/register.go
@@ -16,5 +16,6 @@ func Register(server grpc.ServiceRegistrar, services *Services) {
appv1.RegisterPlansServer(server, services.PlansServer)
appv1.RegisterPaymentsServer(server, services.PaymentsServer)
appv1.RegisterVideosServer(server, services.VideosServer)
+ appv1.RegisterVideoMetadataServer(server, services.VideoMetadataServer)
appv1.RegisterAdminServer(server, services.AdminServer)
}
diff --git a/internal/service/service_admin_jobs_agents.go b/internal/service/service_admin_jobs_agents.go
index 74f7344..812bb26 100644
--- a/internal/service/service_admin_jobs_agents.go
+++ b/internal/service/service_admin_jobs_agents.go
@@ -171,6 +171,88 @@ func (s *appServices) RetryAdminJob(ctx context.Context, req *appv1.RetryAdminJo
}
return &appv1.RetryAdminJobResponse{Job: buildAdminJob(job)}, nil
}
+
+func (s *appServices) ListAdminDlqJobs(ctx context.Context, req *appv1.ListAdminDlqJobsRequest) (*appv1.ListAdminDlqJobsResponse, error) {
+ if _, err := s.requireAdmin(ctx); err != nil {
+ return nil, err
+ }
+ if s.videoWorkflowService == nil {
+ return nil, status.Error(codes.Unavailable, "Job service is unavailable")
+ }
+ offset := int(req.GetOffset())
+ limit := int(req.GetLimit())
+ items, total, err := s.videoWorkflowService.ListDLQ(ctx, offset, limit)
+ if err != nil {
+ return nil, status.Error(codes.Internal, "Failed to list DLQ jobs")
+ }
+ entries := make([]*appv1.AdminDlqEntry, 0, len(items))
+ for _, item := range items {
+ entries = append(entries, buildAdminDlqEntry(item))
+ }
+ return &appv1.ListAdminDlqJobsResponse{Items: entries, Total: total, Offset: int32(offset), Limit: int32(limit)}, nil
+}
+
+func (s *appServices) GetAdminDlqJob(ctx context.Context, req *appv1.GetAdminDlqJobRequest) (*appv1.GetAdminDlqJobResponse, error) {
+ if _, err := s.requireAdmin(ctx); err != nil {
+ return nil, err
+ }
+ if s.videoWorkflowService == nil {
+ return nil, status.Error(codes.Unavailable, "Job service is unavailable")
+ }
+ id := strings.TrimSpace(req.GetId())
+ if id == "" {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ item, err := s.videoWorkflowService.GetDLQ(ctx, id)
+ if err != nil {
+ if strings.Contains(strings.ToLower(err.Error()), "not found") {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ return nil, status.Error(codes.Internal, "Failed to load DLQ job")
+ }
+ return &appv1.GetAdminDlqJobResponse{Item: buildAdminDlqEntry(item)}, nil
+}
+
+func (s *appServices) RetryAdminDlqJob(ctx context.Context, req *appv1.RetryAdminDlqJobRequest) (*appv1.RetryAdminDlqJobResponse, error) {
+ if _, err := s.requireAdmin(ctx); err != nil {
+ return nil, err
+ }
+ if s.videoWorkflowService == nil {
+ return nil, status.Error(codes.Unavailable, "Job service is unavailable")
+ }
+ id := strings.TrimSpace(req.GetId())
+ if id == "" {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ job, err := s.videoWorkflowService.RetryDLQ(ctx, id)
+ if err != nil {
+ if strings.Contains(strings.ToLower(err.Error()), "not found") {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ return nil, status.Error(codes.FailedPrecondition, err.Error())
+ }
+ return &appv1.RetryAdminDlqJobResponse{Job: buildAdminJob(job)}, nil
+}
+
+func (s *appServices) RemoveAdminDlqJob(ctx context.Context, req *appv1.RemoveAdminDlqJobRequest) (*appv1.RemoveAdminDlqJobResponse, error) {
+ if _, err := s.requireAdmin(ctx); err != nil {
+ return nil, err
+ }
+ if s.videoWorkflowService == nil {
+ return nil, status.Error(codes.Unavailable, "Job service is unavailable")
+ }
+ id := strings.TrimSpace(req.GetId())
+ if id == "" {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ if err := s.videoWorkflowService.RemoveDLQ(ctx, id); err != nil {
+ if strings.Contains(strings.ToLower(err.Error()), "not found") {
+ return nil, status.Error(codes.NotFound, "Job not found in DLQ")
+ }
+ return nil, status.Error(codes.Internal, "Failed to remove DLQ job")
+ }
+ return &appv1.RemoveAdminDlqJobResponse{Status: "removed", JobId: id}, nil
+}
func (s *appServices) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
diff --git a/internal/service/service_core.go b/internal/service/service_core.go
index 5413aa2..701a5e6 100644
--- a/internal/service/service_core.go
+++ b/internal/service/service_core.go
@@ -53,6 +53,7 @@ type Services struct {
appv1.PlansServer
appv1.PaymentsServer
appv1.VideosServer
+ appv1.VideoMetadataServer
appv1.AdminServer
}
@@ -79,6 +80,7 @@ type appServices struct {
appv1.UnimplementedPlansServer
appv1.UnimplementedPaymentsServer
appv1.UnimplementedVideosServer
+ appv1.UnimplementedVideoMetadataServer
appv1.UnimplementedAdminServer
db *gorm.DB
@@ -104,7 +106,6 @@ type appServices struct {
googleOauth *oauth2.Config
googleStateTTL time.Duration
googleUserInfoURL string
- frontendBaseURL string
jobRepository JobRepository
}
@@ -172,11 +173,6 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi
}
}
- frontendBaseURL := ""
- if cfg != nil {
- frontendBaseURL = cfg.Frontend.BaseURL
- }
-
service := &appServices{
db: db,
logger: l,
@@ -202,7 +198,6 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi
googleOauth: googleOauth,
googleStateTTL: googleStateTTL,
googleUserInfoURL: defaultGoogleUserInfoURL,
- frontendBaseURL: frontendBaseURL,
}
return &Services{
AuthServer: &authAppService{appServices: service},
@@ -215,6 +210,7 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi
PlansServer: &plansAppService{appServices: service},
PaymentsServer: &paymentsAppService{appServices: service},
VideosServer: &videosAppService{appServices: service},
+ VideoMetadataServer: &videosAppService{appServices: service},
AdminServer: &adminAppService{appServices: service},
}
}
diff --git a/internal/service/service_job.go b/internal/service/service_job.go
index 56f3f19..f9dbb6e 100644
--- a/internal/service/service_job.go
+++ b/internal/service/service_job.go
@@ -12,9 +12,11 @@ import (
"time"
"gorm.io/gorm"
+ redisadapter "stream.api/internal/adapters/redis"
"stream.api/internal/database/model"
"stream.api/internal/dto"
"stream.api/internal/repository"
+ "stream.api/pkg/logger"
)
type JobQueue interface {
@@ -33,20 +35,36 @@ type LogPubSub interface {
SubscribeJobUpdates(ctx context.Context) (<-chan string, error)
}
+type DeadLetterQueue interface {
+ Add(ctx context.Context, job *model.Job, reason string) error
+ Get(ctx context.Context, jobID string) (*redisadapter.DLQEntry, error)
+ List(ctx context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error)
+ Count(ctx context.Context) (int64, error)
+ Remove(ctx context.Context, jobID string) error
+ Retry(ctx context.Context, jobID string) (*model.Job, error)
+}
+
type JobService struct {
queue JobQueue
pubsub LogPubSub
+ dlq DeadLetterQueue
jobRepository JobRepository
+ logger logger.Logger
}
-func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub) *JobService {
+func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub, dlq DeadLetterQueue) *JobService {
return &JobService{
queue: queue,
pubsub: pubsub,
+ dlq: dlq,
jobRepository: repository.NewJobRepository(db),
}
}
+func (s *JobService) SetLogger(l logger.Logger) {
+ s.logger = l
+}
+
var ErrInvalidJobCursor = errors.New("invalid job cursor")
func strPtr(v string) *string { return &v }
@@ -165,11 +183,16 @@ func (s *JobService) CreateJob(ctx context.Context, userID string, videoID strin
if err := syncVideoStatus(ctx, s.jobRepository, videoID, dto.JobStatusPending); err != nil {
return nil, err
}
- // dtoJob := todtoJob(job)
+ if s.queue == nil {
+ return nil, errors.New("job queue is unavailable")
+ }
+ if err := s.removeFromQueue(ctx, job.ID); err != nil {
+ return nil, err
+ }
if err := s.queue.Enqueue(ctx, job); err != nil {
return nil, err
}
- _ = s.pubsub.PublishJobUpdate(ctx, job.ID, status, videoID)
+ _ = s.publishJobUpdate(ctx, job.ID, status, videoID)
return job, nil
}
@@ -233,18 +256,55 @@ func (s *JobService) GetJob(ctx context.Context, id string) (*model.Job, error)
}
func (s *JobService) GetNextJob(ctx context.Context) (*model.Job, error) {
- return s.queue.Dequeue(ctx)
+ if s.queue == nil {
+ return nil, errors.New("job queue is unavailable")
+ }
+
+ for {
+ job, err := s.queue.Dequeue(ctx)
+ if err != nil || job == nil {
+ return job, err
+ }
+
+ fresh, err := s.jobRepository.GetByID(ctx, job.ID)
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ continue
+ }
+ return nil, err
+ }
+ if !s.canDispatchJob(fresh) {
+ continue
+ }
+ return fresh, nil
+ }
}
+
func (s *JobService) SubscribeSystemResources(ctx context.Context) (<-chan dto.SystemResource, error) {
+ if s.pubsub == nil {
+ return nil, errors.New("job pubsub is unavailable")
+ }
return s.pubsub.SubscribeResources(ctx)
}
+
func (s *JobService) SubscribeJobLogs(ctx context.Context, jobID string) (<-chan dto.LogEntry, error) {
+ if s.pubsub == nil {
+ return nil, errors.New("job pubsub is unavailable")
+ }
return s.pubsub.Subscribe(ctx, jobID)
}
+
func (s *JobService) SubscribeCancel(ctx context.Context, agentID string) (<-chan string, error) {
+ if s.pubsub == nil {
+ return nil, errors.New("job pubsub is unavailable")
+ }
return s.pubsub.SubscribeCancel(ctx, agentID)
}
+
func (s *JobService) SubscribeJobUpdates(ctx context.Context) (<-chan string, error) {
+ if s.pubsub == nil {
+ return nil, errors.New("job pubsub is unavailable")
+ }
return s.pubsub.SubscribeJobUpdates(ctx)
}
@@ -253,17 +313,38 @@ func (s *JobService) UpdateJobStatus(ctx context.Context, jobID string, status d
if err != nil {
return err
}
+ currentStatus := s.jobStatus(job)
+ if (currentStatus == dto.JobStatusCancelled || currentStatus == dto.JobStatusSuccess) && currentStatus != status {
+ return nil
+ }
+ if currentStatus == status {
+ return nil
+ }
+
now := time.Now()
- job.Status = strPtr(string(status))
- job.UpdatedAt = &now
- if err := s.jobRepository.Save(ctx, job); err != nil {
+ updated := false
+ switch status {
+ case dto.JobStatusRunning:
+ updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusPending)}, string(status), now, false)
+ case dto.JobStatusSuccess:
+ updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusRunning)}, string(status), now, true)
+ if err == nil && updated {
+ err = s.removeFromQueue(ctx, jobID)
+ }
+ default:
+ updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(currentStatus)}, string(status), now, status == dto.JobStatusFailure || status == dto.JobStatusCancelled)
+ }
+ if err != nil {
return err
}
+ if !updated {
+ return nil
+ }
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, status); err != nil {
return err
}
- return s.pubsub.PublishJobUpdate(ctx, jobID, string(status), cfg.VideoID)
+ return s.publishJobUpdate(ctx, jobID, string(status), cfg.VideoID)
}
func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string) error {
@@ -271,23 +352,26 @@ func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string
if err != nil {
return err
}
+ if !s.canDispatchJob(job) {
+ return fmt.Errorf("job %s is not dispatchable", jobID)
+ }
+
agentNumeric, err := strconv.ParseInt(agentID, 10, 64)
if err != nil {
return err
}
- now := time.Now()
- status := string(dto.JobStatusRunning)
- job.AgentID = &agentNumeric
- job.Status = &status
- job.UpdatedAt = &now
- if err := s.jobRepository.Save(ctx, job); err != nil {
+ updated, err := s.jobRepository.AssignPendingJob(ctx, jobID, agentNumeric, time.Now())
+ if err != nil {
return err
}
+ if !updated {
+ return fmt.Errorf("job %s is not dispatchable", jobID)
+ }
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusRunning); err != nil {
return err
}
- return s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID)
+ return s.publishJobUpdate(ctx, jobID, string(dto.JobStatusRunning), cfg.VideoID)
}
func (s *JobService) CancelJob(ctx context.Context, jobID string) error {
@@ -295,31 +379,25 @@ func (s *JobService) CancelJob(ctx context.Context, jobID string) error {
if err != nil {
return fmt.Errorf("job not found: %w", err)
}
- currentStatus := ""
- if job.Status != nil {
- currentStatus = *job.Status
- }
- if currentStatus != string(dto.JobStatusPending) && currentStatus != string(dto.JobStatusRunning) {
- return fmt.Errorf("cannot cancel job with status %s", currentStatus)
- }
- cancelled := true
- status := string(dto.JobStatusCancelled)
- now := time.Now()
- job.Cancelled = &cancelled
- job.Status = &status
- job.UpdatedAt = &now
- if err := s.jobRepository.Save(ctx, job); err != nil {
+ updated, err := s.jobRepository.CancelJobIfActive(ctx, jobID, time.Now())
+ if err != nil {
return err
}
+ if !updated {
+ return fmt.Errorf("cannot cancel job with status %s", s.jobStatus(job))
+ }
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusCancelled); err != nil {
return err
}
- _ = s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID)
- if job.AgentID != nil {
+ _ = s.publishJobUpdate(ctx, jobID, string(dto.JobStatusCancelled), cfg.VideoID)
+ if err := s.removeFromQueue(ctx, job.ID); err != nil {
+ return err
+ }
+ if job.AgentID != nil && s.pubsub != nil {
_ = s.pubsub.PublishCancel(ctx, strconv.FormatInt(*job.AgentID, 10), job.ID)
}
- return s.pubsub.Publish(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1)
+ return s.publishLog(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1)
}
func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, error) {
@@ -327,47 +405,17 @@ func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, er
if err != nil {
return nil, fmt.Errorf("job not found: %w", err)
}
- currentStatus := ""
- if job.Status != nil {
- currentStatus = *job.Status
- }
- if currentStatus != string(dto.JobStatusFailure) && currentStatus != string(dto.JobStatusCancelled) {
+ currentStatus := s.jobStatus(job)
+ if currentStatus != dto.JobStatusFailure && currentStatus != dto.JobStatusCancelled {
return nil, fmt.Errorf("cannot retry job with status %s", currentStatus)
}
- currentRetry := int64(0)
- if job.RetryCount != nil {
- currentRetry = *job.RetryCount
- }
- maxRetries := int64(3)
- if job.MaxRetries != nil {
- maxRetries = *job.MaxRetries
- }
- if currentRetry >= maxRetries {
- return nil, fmt.Errorf("max retries (%d) exceeded", maxRetries)
- }
- pending := string(dto.JobStatusPending)
- cancelled := false
- progress := 0.0
- now := time.Now()
- job.Status = &pending
- job.Cancelled = &cancelled
- job.RetryCount = int64Ptr(currentRetry + 1)
- job.Progress = &progress
- job.AgentID = nil
- job.UpdatedAt = &now
- if err := s.jobRepository.Save(ctx, job); err != nil {
+ if err := s.requeueJob(ctx, job, false); err != nil {
return nil, err
}
- cfg := parseJobConfig(job.Config)
- if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil {
- return nil, err
+ if s.dlq != nil {
+ _ = s.dlq.Remove(ctx, jobID)
}
- // dtoJob := todtoJob(job)
- if err := s.queue.Enqueue(ctx, job); err != nil {
- return nil, err
- }
- _ = s.pubsub.PublishJobUpdate(ctx, jobID, pending, cfg.VideoID)
- return job, nil
+ return s.jobRepository.GetByID(ctx, jobID)
}
func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progress float64) error {
@@ -381,7 +429,7 @@ func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progre
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
- return s.pubsub.Publish(ctx, jobID, "", progress)
+ return s.publishLog(ctx, jobID, "", progress)
}
func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byte) error {
@@ -420,7 +468,7 @@ func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byt
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
- return s.pubsub.Publish(ctx, jobID, line, progress)
+ return s.publishLog(ctx, jobID, line, progress)
}
func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID string, status dto.JobStatus) error {
@@ -444,5 +492,345 @@ func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID s
}
func (s *JobService) PublishSystemResources(ctx context.Context, agentID string, data []byte) error {
+ if s.pubsub == nil {
+ return errors.New("job pubsub is unavailable")
+ }
return s.pubsub.PublishResource(ctx, agentID, data)
}
+
+func (s *JobService) StartInflightReclaimLoop(ctx context.Context, interval time.Duration, batchSize int64) {
+ if interval <= 0 {
+ interval = 30 * time.Second
+ }
+ if s.logger != nil {
+ s.logger.Info("started inflight reclaim loop", "interval", interval.String(), "batch_size", batchSize)
+ }
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ if s.logger != nil {
+ s.logger.Info("stopped inflight reclaim loop")
+ }
+ return
+ case <-ticker.C:
+ _ = s.reclaimExpiredOnce(ctx, batchSize)
+ }
+ }
+}
+
+func (s *JobService) reclaimExpiredOnce(ctx context.Context, batchSize int64) error {
+ type expirable interface {
+ ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error)
+ }
+ queue, ok := s.queue.(expirable)
+ if !ok {
+ return nil
+ }
+ startedAt := time.Now()
+ jobIDs, err := queue.ListExpiredInflight(ctx, time.Now(), batchSize)
+ if err != nil {
+ if s.logger != nil {
+ s.logger.Error("failed to list expired inflight jobs", "error", err)
+ }
+ return err
+ }
+ for _, jobID := range jobIDs {
+ _ = s.handleExpiredInflightJob(ctx, jobID)
+ }
+ if s.logger != nil && len(jobIDs) > 0 {
+ s.logger.Info("completed inflight reclaim batch", "expired_count", len(jobIDs), "duration_ms", time.Since(startedAt).Milliseconds())
+ }
+ return nil
+}
+
+func (s *JobService) handleExpiredInflightJob(ctx context.Context, jobID string) error {
+ job, err := s.jobRepository.GetByID(ctx, jobID)
+ if err != nil {
+ if s.logger != nil {
+ s.logger.Warn("failed to load expired inflight job", "job_id", jobID, "error", err)
+ }
+ return err
+ }
+ status := s.jobStatus(job)
+ if s.logger != nil {
+ s.logger.Warn("processing expired inflight job", "job_id", jobID, "current_status", status)
+ }
+ switch status {
+ case dto.JobStatusRunning:
+ return s.HandleJobFailure(ctx, jobID, "lease_expired")
+ case dto.JobStatusPending, dto.JobStatusSuccess, dto.JobStatusFailure, dto.JobStatusCancelled:
+ return s.removeFromQueue(ctx, jobID)
+ default:
+ return s.removeFromQueue(ctx, jobID)
+ }
+}
+
+func (s *JobService) RenewJobLease(ctx context.Context, jobID string) error {
+ type touchable interface {
+ TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error
+ }
+ queue, ok := s.queue.(touchable)
+ if !ok {
+ return nil
+ }
+ return queue.TouchInflight(ctx, jobID, 15*time.Minute)
+}
+
+func (s *JobService) HandleDispatchFailure(ctx context.Context, jobID string, reason string, retryable bool) error {
+ job, err := s.jobRepository.GetByID(ctx, jobID)
+ if err != nil {
+ return err
+ }
+ if !retryable {
+ return s.moveJobToDLQ(ctx, job, reason)
+ }
+ return s.requeueOrDLQ(ctx, job, reason)
+}
+
+func (s *JobService) HandleJobFailure(ctx context.Context, jobID string, reason string) error {
+ job, err := s.jobRepository.GetByID(ctx, jobID)
+ if err != nil {
+ return err
+ }
+ return s.requeueOrDLQ(ctx, job, reason)
+}
+
+func (s *JobService) HandleAgentDisconnect(ctx context.Context, jobID string) error {
+ job, err := s.jobRepository.GetByID(ctx, jobID)
+ if err != nil {
+ return err
+ }
+ return s.requeueOrDLQ(ctx, job, "agent_unregistered")
+}
+
+func (s *JobService) requeueOrDLQ(ctx context.Context, job *model.Job, reason string) error {
+ if job == nil {
+ return nil
+ }
+ willRetry := s.canAutoRetry(job)
+ if s.logger != nil {
+ s.logger.Warn("evaluating retry vs dlq", "job_id", job.ID, "reason", reason, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "will_retry", willRetry)
+ }
+ if !willRetry {
+ return s.moveJobToDLQ(ctx, job, reason)
+ }
+ return s.requeueJob(ctx, job, true)
+}
+
+func (s *JobService) requeueJob(ctx context.Context, job *model.Job, incrementRetry bool) error {
+ if job == nil {
+ return nil
+ }
+ if s.queue == nil {
+ return errors.New("job queue is unavailable")
+ }
+
+ pending := string(dto.JobStatusPending)
+ cancelled := false
+ progress := 0.0
+ now := time.Now()
+ job.Status = &pending
+ job.Cancelled = &cancelled
+ job.Progress = &progress
+ job.AgentID = nil
+ job.UpdatedAt = &now
+ if incrementRetry {
+ job.RetryCount = int64Ptr(s.retryCount(job) + 1)
+ job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Auto-retry scheduled at %s", now.UTC().Format(time.RFC3339)))
+ }
+ if err := s.jobRepository.Save(ctx, job); err != nil {
+ return err
+ }
+ cfg := parseJobConfig(job.Config)
+ if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil {
+ return err
+ }
+ if err := s.queue.Enqueue(ctx, job); err != nil {
+ return err
+ }
+ if s.logger != nil {
+ s.logger.Info("requeued job", "job_id", job.ID, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "action", "requeue")
+ }
+ return s.publishJobUpdate(ctx, job.ID, pending, cfg.VideoID)
+}
+
+func (s *JobService) moveJobToDLQ(ctx context.Context, job *model.Job, reason string) error {
+ if job == nil {
+ return nil
+ }
+ failure := string(dto.JobStatusFailure)
+ now := time.Now()
+ job.Status = &failure
+ job.AgentID = nil
+ job.UpdatedAt = &now
+ job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Sent to DLQ: %s", strings.TrimSpace(reason)))
+ if err := s.jobRepository.Save(ctx, job); err != nil {
+ return err
+ }
+ cfg := parseJobConfig(job.Config)
+ if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusFailure); err != nil {
+ return err
+ }
+ if err := s.removeFromQueue(ctx, job.ID); err != nil {
+ return err
+ }
+ if s.dlq != nil {
+ if err := s.dlq.Add(ctx, job, strings.TrimSpace(reason)); err != nil {
+ return err
+ }
+ if s.logger != nil {
+ dlqCount, _ := s.dlq.Count(ctx)
+ s.logger.Warn("moved job to dlq", "job_id", job.ID, "reason", strings.TrimSpace(reason), "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "dlq_size", dlqCount)
+ }
+ }
+ return s.publishJobUpdate(ctx, job.ID, failure, cfg.VideoID)
+}
+
+func (s *JobService) canDispatchJob(job *model.Job) bool {
+ if job == nil {
+ return false
+ }
+ if job.Cancelled != nil && *job.Cancelled {
+ return false
+ }
+ return s.jobStatus(job) == dto.JobStatusPending
+}
+
+func (s *JobService) canAutoRetry(job *model.Job) bool {
+ return s.retryCount(job) < s.maxRetries(job)
+}
+
+func (s *JobService) retryCount(job *model.Job) int64 {
+ if job == nil || job.RetryCount == nil {
+ return 0
+ }
+ return *job.RetryCount
+}
+
+func (s *JobService) maxRetries(job *model.Job) int64 {
+ if job == nil || job.MaxRetries == nil || *job.MaxRetries <= 0 {
+ return 3
+ }
+ return *job.MaxRetries
+}
+
+func (s *JobService) jobStatus(job *model.Job) dto.JobStatus {
+ if job == nil || job.Status == nil {
+ return dto.JobStatusPending
+ }
+ return dto.JobStatus(strings.TrimSpace(*job.Status))
+}
+
+func (s *JobService) publishJobUpdate(ctx context.Context, jobID string, status string, videoID string) error {
+ if s.pubsub == nil {
+ return nil
+ }
+ return s.pubsub.PublishJobUpdate(ctx, jobID, status, videoID)
+}
+
+func (s *JobService) publishLog(ctx context.Context, jobID string, logLine string, progress float64) error {
+ if s.pubsub == nil {
+ return nil
+ }
+ return s.pubsub.Publish(ctx, jobID, logLine, progress)
+}
+
+func (s *JobService) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) {
+ if s.dlq == nil {
+ return []*dto.DLQEntry{}, 0, nil
+ }
+ if offset < 0 {
+ offset = 0
+ }
+ if limit <= 0 {
+ limit = 20
+ }
+ entries, err := s.dlq.List(ctx, int64(offset), int64(limit))
+ if err != nil {
+ return nil, 0, err
+ }
+ count, err := s.dlq.Count(ctx)
+ if err != nil {
+ return nil, 0, err
+ }
+ items := make([]*dto.DLQEntry, 0, len(entries))
+ for _, entry := range entries {
+ items = append(items, &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount})
+ }
+ return items, count, nil
+}
+
+func (s *JobService) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) {
+ if s.dlq == nil {
+ return nil, fmt.Errorf("job not found in DLQ")
+ }
+ entry, err := s.dlq.Get(ctx, strings.TrimSpace(id))
+ if err != nil {
+ return nil, err
+ }
+ return &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount}, nil
+}
+
+func (s *JobService) RetryDLQ(ctx context.Context, id string) (*model.Job, error) {
+ if s.dlq == nil {
+ return nil, fmt.Errorf("job not found in DLQ")
+ }
+ job, err := s.dlq.Retry(ctx, strings.TrimSpace(id))
+ if err != nil {
+ return nil, err
+ }
+ if job == nil {
+ return nil, fmt.Errorf("job not found in DLQ")
+ }
+ if err := s.requeueJob(ctx, job, false); err != nil {
+ return nil, err
+ }
+ if s.logger != nil {
+ count, _ := s.dlq.Count(ctx)
+ s.logger.Info("retried job from dlq", "job_id", job.ID, "dlq_size", count)
+ }
+ return s.jobRepository.GetByID(ctx, job.ID)
+}
+
+func (s *JobService) RemoveDLQ(ctx context.Context, id string) error {
+ if s.dlq == nil {
+ return fmt.Errorf("job not found in DLQ")
+ }
+ jobID := strings.TrimSpace(id)
+ if err := s.dlq.Remove(ctx, jobID); err != nil {
+ return err
+ }
+ if s.logger != nil {
+ count, _ := s.dlq.Count(ctx)
+ s.logger.Info("removed job from dlq", "job_id", jobID, "dlq_size", count)
+ }
+ return nil
+}
+
+func (s *JobService) removeFromQueue(ctx context.Context, jobID string) error {
+ type ackable interface {
+ Ack(ctx context.Context, jobID string) error
+ }
+ if queue, ok := s.queue.(ackable); ok {
+ return queue.Ack(ctx, jobID)
+ }
+ return nil
+}
+
+func appendSystemLog(logs *string, line string) *string {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ return logs
+ }
+ existing := ""
+ if logs != nil {
+ existing = *logs
+ }
+ if existing != "" && !strings.HasSuffix(existing, "\n") {
+ existing += "\n"
+ }
+ existing += line + "\n"
+ return &existing
+}
diff --git a/internal/service/service_popup_ads_test.go b/internal/service/service_popup_ads_test.go
index 8c98b1f..fcf8b6d 100644
--- a/internal/service/service_popup_ads_test.go
+++ b/internal/service/service_popup_ads_test.go
@@ -7,12 +7,12 @@ import (
"time"
"github.com/google/uuid"
- _ "modernc.org/sqlite"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
+ _ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
@@ -152,9 +152,9 @@ func popupTestContext(userID, role string) context.Context {
))
}
-func popupPtrString(v string) *string { return &v }
-func popupPtrBool(v bool) *bool { return &v }
-func popupPtrInt32(v int32) *int32 { return &v }
+func popupPtrString(v string) *string { return &v }
+func popupPtrBool(v bool) *bool { return &v }
+func popupPtrInt32(v int32) *int32 { return &v }
func popupPtrTime(v time.Time) *time.Time { return &v }
func popupSeedUser(t *testing.T, db *gorm.DB, user model.User) model.User {
@@ -209,10 +209,10 @@ func TestPopupAdsUserFlow(t *testing.T) {
user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")})
createResp, err := (&popupAdsAppService{appServices: services}).CreatePopupAd(popupTestContext(user.ID, "USER"), &appv1.CreatePopupAdRequest{
- Type: "url",
- Label: "Homepage Campaign",
- Value: "https://example.com/landing",
- IsActive: popupPtrBool(true),
+ Type: "url",
+ Label: "Homepage Campaign",
+ Value: "https://example.com/landing",
+ IsActive: popupPtrBool(true),
MaxTriggersPerSession: popupPtrInt32(5),
})
if err != nil {
@@ -231,11 +231,11 @@ func TestPopupAdsUserFlow(t *testing.T) {
}
updateResp, err := (&popupAdsAppService{appServices: services}).UpdatePopupAd(popupTestContext(user.ID, "USER"), &appv1.UpdatePopupAdRequest{
- Id: createResp.Item.Id,
- Type: "script",
- Label: "Homepage Campaign v2",
- Value: ``,
- IsActive: popupPtrBool(false),
+ Id: createResp.Item.Id,
+ Type: "script",
+ Label: "Homepage Campaign v2",
+ Value: ``,
+ IsActive: popupPtrBool(false),
MaxTriggersPerSession: popupPtrInt32(8),
})
if err != nil {
@@ -302,11 +302,11 @@ func TestPopupAdsAdminFlow(t *testing.T) {
user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")})
createResp, err := services.CreateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.CreateAdminPopupAdRequest{
- UserId: user.ID,
- Type: "url",
- Label: "Admin Campaign",
- Value: "https://example.com/admin",
- IsActive: popupPtrBool(true),
+ UserId: user.ID,
+ Type: "url",
+ Label: "Admin Campaign",
+ Value: "https://example.com/admin",
+ IsActive: popupPtrBool(true),
MaxTriggersPerSession: popupPtrInt32(7),
})
if err != nil {
@@ -325,12 +325,12 @@ func TestPopupAdsAdminFlow(t *testing.T) {
}
updateResp, err := services.UpdateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.UpdateAdminPopupAdRequest{
- Id: createResp.Item.Id,
- UserId: user.ID,
- Type: "script",
- Label: "Admin Campaign v2",
- Value: ``,
- IsActive: popupPtrBool(false),
+ Id: createResp.Item.Id,
+ UserId: user.ID,
+ Type: "script",
+ Label: "Admin Campaign v2",
+ Value: ``,
+ IsActive: popupPtrBool(false),
MaxTriggersPerSession: popupPtrInt32(11),
})
if err != nil {
diff --git a/internal/service/service_videos.go b/internal/service/service_videos.go
index 4bf69b4..70b2de8 100644
--- a/internal/service/service_videos.go
+++ b/internal/service/service_videos.go
@@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/status"
"gorm.io/gorm"
appv1 "stream.api/internal/api/proto/app/v1"
+ "stream.api/internal/database/model"
)
func (s *videosAppService) GetUploadUrl(ctx context.Context, req *appv1.GetUploadUrlRequest) (*appv1.GetUploadUrlResponse, error) {
@@ -151,6 +152,94 @@ func (s *videosAppService) GetVideo(ctx context.Context, req *appv1.GetVideoRequ
}
return &appv1.GetVideoResponse{Video: payload}, nil
}
+
+func (s *videosAppService) GetVideoMetadata(ctx context.Context, req *appv1.GetVideoMetadataRequest) (*appv1.GetVideoMetadataResponse, error) {
+ if _, err := s.authenticator.RequireTrustedMetadata(ctx); err != nil {
+ return nil, err
+ }
+ videoID := strings.TrimSpace(req.GetVideoId())
+ if videoID == "" {
+ return nil, status.Error(codes.NotFound, "Video not found")
+ }
+ if s.videoRepository == nil {
+ return nil, status.Error(codes.Internal, "Video repository is unavailable")
+ }
+ video, err := s.videoRepository.GetByID(ctx, videoID)
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, status.Error(codes.NotFound, "Video not found")
+ }
+ s.logger.Error("Failed to fetch video metadata source video", "error", err, "video_id", videoID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ videoPayload, err := s.buildVideo(ctx, video)
+ if err != nil {
+ s.logger.Error("Failed to build video metadata video payload", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+
+ ownerID := strings.TrimSpace(video.UserID)
+ ownerUser, err := s.userRepository.GetByID(ctx, ownerID)
+ if err != nil {
+ s.logger.Error("Failed to load video owner for metadata", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ configOwnerID := ownerID
+ if ownerUser.PlanID == nil || strings.TrimSpace(*ownerUser.PlanID) == "" {
+ configOwnerID, err = s.resolveSystemConfigOwnerID(ctx)
+ if err != nil {
+ s.logger.Error("Failed to resolve system config owner", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ }
+
+ domains, err := s.domainRepository.ListByUser(ctx, ownerID)
+ if err != nil {
+ s.logger.Error("Failed to load video metadata domains", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ protoDomains := make([]*appv1.Domain, 0, len(domains))
+ for i := range domains {
+ item := domains[i]
+ protoDomains = append(protoDomains, toProtoDomain(&item))
+ }
+
+ playerConfig, err := s.resolveDefaultPlayerConfig(ctx, configOwnerID)
+ if err != nil {
+ s.logger.Error("Failed to load default player config for video metadata", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ if playerConfig == nil {
+ return nil, status.Error(codes.FailedPrecondition, "Default player config is required")
+ }
+ popupAd, err := s.resolveActivePopupAd(ctx, configOwnerID)
+ if err != nil {
+ s.logger.Error("Failed to load popup ad for video metadata", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ if popupAd == nil {
+ return nil, status.Error(codes.FailedPrecondition, "Active popup ad is required")
+ }
+ adTemplate, err := s.resolveEffectiveAdTemplate(ctx, video, ownerID, configOwnerID)
+ if err != nil {
+ s.logger.Error("Failed to load ad template for video metadata", "error", err, "video_id", video.ID)
+ return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
+ }
+ if adTemplate == nil {
+ return nil, status.Error(codes.FailedPrecondition, "Ad template is required")
+ }
+ if len(protoDomains) == 0 {
+ return nil, status.Error(codes.FailedPrecondition, "At least one domain is required")
+ }
+
+ return &appv1.GetVideoMetadataResponse{
+ Video: videoPayload,
+ DefaultPlayerConfig: toProtoPlayerConfig(playerConfig),
+ AdTemplate: toProtoAdTemplate(adTemplate),
+ ActivePopupAd: toProtoPopupAd(popupAd),
+ Domains: protoDomains,
+ }, nil
+}
func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
@@ -216,6 +305,59 @@ func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVid
}
return &appv1.UpdateVideoResponse{Video: payload}, nil
}
+func (s *videosAppService) resolveSystemConfigOwnerID(ctx context.Context) (string, error) {
+ users, _, err := s.userRepository.ListForAdmin(ctx, "", "ADMIN", 1, 0)
+ if err != nil {
+ return "", err
+ }
+ if len(users) == 0 {
+ return "", fmt.Errorf("system config owner not found")
+ }
+ return users[0].ID, nil
+}
+
+func (s *videosAppService) resolveDefaultPlayerConfig(ctx context.Context, userID string) (*model.PlayerConfig, error) {
+ items, err := s.playerConfigRepo.ListByUser(ctx, userID)
+ if err != nil {
+ return nil, err
+ }
+ if len(items) == 0 {
+ return nil, nil
+ }
+ return &items[0], nil
+}
+
+func (s *videosAppService) resolveActivePopupAd(ctx context.Context, userID string) (*model.PopupAd, error) {
+ item, err := s.popupAdRepository.GetActiveByUser(ctx, userID)
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return item, nil
+}
+
+func (s *videosAppService) resolveEffectiveAdTemplate(ctx context.Context, video *model.Video, ownerID string, configOwnerID string) (*model.AdTemplate, error) {
+ if video != nil && video.AdID != nil && strings.TrimSpace(*video.AdID) != "" {
+ item, err := s.adTemplateRepository.GetByIDAndUser(ctx, strings.TrimSpace(*video.AdID), ownerID)
+ if err == nil {
+ return item, nil
+ }
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ }
+ items, err := s.adTemplateRepository.ListByUser(ctx, configOwnerID)
+ if err != nil {
+ return nil, err
+ }
+ if len(items) == 0 {
+ return nil, nil
+ }
+ return &items[0], nil
+}
+
func (s *videosAppService) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
diff --git a/internal/transport/grpc/agent_lifecycle.go b/internal/transport/grpc/agent_lifecycle.go
index fdf1d7e..f73bcf7 100644
--- a/internal/transport/grpc/agent_lifecycle.go
+++ b/internal/transport/grpc/agent_lifecycle.go
@@ -7,7 +7,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
proto "stream.api/internal/api/proto/agent/v1"
- "stream.api/internal/dto"
)
func (s *Server) RegisterAgent(ctx context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) {
@@ -39,7 +38,7 @@ func (s *Server) UnregisterAgent(ctx context.Context, _ *proto.Empty) (*proto.Em
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
for _, jobID := range s.getAgentJobs(agentID) {
- _ = s.jobService.UpdateJobStatus(ctx, jobID, dto.JobStatusFailure)
+ _ = s.jobService.HandleAgentDisconnect(ctx, jobID)
s.untrackJobAssignment(agentID, jobID)
}
s.sessions.Delete(token)
diff --git a/internal/transport/grpc/auth.go b/internal/transport/grpc/auth.go
index 39e373d..7f4fca3 100644
--- a/internal/transport/grpc/auth.go
+++ b/internal/transport/grpc/auth.go
@@ -41,7 +41,7 @@ func (s *Server) getAgentIDFromContext(ctx context.Context) (string, string, boo
func (s *Server) Auth(ctx context.Context, req *proto.AuthRequest) (*proto.AuthResponse, error) {
if s.agentSecret != "" && req.AgentToken != s.agentSecret {
- return nil, status.Error(codes.Unauthenticated, "invalid agent secret")
+ return nil, status.Error(codes.Unauthenticated, "invalid internal marker")
}
agentID := req.AgentId
if len(agentID) > 6 && agentID[:6] == "agent-" {
diff --git a/internal/transport/grpc/server.go b/internal/transport/grpc/server.go
index b5f90cc..24fbfb9 100644
--- a/internal/transport/grpc/server.go
+++ b/internal/transport/grpc/server.go
@@ -3,6 +3,7 @@ package grpc
import (
"context"
"net"
+ "time"
grpcpkg "google.golang.org/grpc"
"gorm.io/gorm"
@@ -21,11 +22,14 @@ type GRPCModule struct {
mqttPublisher *mqtt.MQTTBootstrap
grpcServer *grpcpkg.Server
cfg *config.Config
+ cancel context.CancelFunc
}
func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *redisadapter.RedisAdapter, appLogger logger.Logger) (*GRPCModule, error) {
- jobService := service.NewJobService(db, rds, rds)
- agentRuntime := NewServer(jobService, cfg.Render.AgentSecret)
+ moduleCtx, cancel := context.WithCancel(ctx)
+ jobService := service.NewJobService(db, rds, rds, redisadapter.NewDeadLetterQueue(rds.Client()))
+ jobService.SetLogger(appLogger)
+ agentRuntime := NewServer(jobService, cfg.Internal.Marker)
videoService := renderworkflow.New(db, jobService)
grpcServer := grpcpkg.NewServer()
@@ -34,6 +38,7 @@ func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *re
agentRuntime: agentRuntime,
grpcServer: grpcServer,
cfg: cfg,
+ cancel: cancel,
}
var notificationPublisher service.NotificationEventPublisher = nil
@@ -50,8 +55,9 @@ func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *re
agentRuntime.Register(grpcServer)
service.Register(grpcServer, service.NewServices(rds, db, appLogger, cfg, videoService, agentRuntime, notificationPublisher))
if module.mqttPublisher != nil {
- module.mqttPublisher.Start(ctx)
+ module.mqttPublisher.Start(moduleCtx)
}
+ go jobService.StartInflightReclaimLoop(moduleCtx, 30*time.Second, 100)
return module, nil
}
@@ -62,6 +68,9 @@ func (m *GRPCModule) GRPCServer() *grpcpkg.Server { return m.grpcServe
func (m *GRPCModule) GRPCAddress() string { return ":" + m.cfg.Server.GRPCPort }
func (m *GRPCModule) ServeGRPC(listener net.Listener) error { return m.grpcServer.Serve(listener) }
func (m *GRPCModule) Shutdown() {
+ if m.cancel != nil {
+ m.cancel()
+ }
if m.grpcServer != nil {
m.grpcServer.GracefulStop()
}
diff --git a/internal/transport/grpc/stream_handlers.go b/internal/transport/grpc/stream_handlers.go
index 1c938a8..c22b741 100644
--- a/internal/transport/grpc/stream_handlers.go
+++ b/internal/transport/grpc/stream_handlers.go
@@ -53,12 +53,14 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami
}
s.trackJobAssignment(agentID, job.ID)
if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil {
+ _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "assign_failed", true)
s.untrackJobAssignment(agentID, job.ID)
continue
}
+
var config map[string]any
- if err := json.Unmarshal([]byte(*job.Config), &config); err != nil {
- _ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusFailure)
+ if job.Config == nil || json.Unmarshal([]byte(*job.Config), &config) != nil {
+ _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "invalid_config", false)
s.untrackJobAssignment(agentID, job.ID)
continue
}
@@ -80,7 +82,7 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami
}
payload, _ := json.Marshal(map[string]any{"image": image, "commands": commands, "environment": map[string]string{}})
if err := stream.Send(&proto.Workflow{Id: job.ID, Timeout: 60 * 60 * 1000, Payload: payload}); err != nil {
- _ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusPending)
+ _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "stream_send_failed", true)
s.untrackJobAssignment(agentID, job.ID)
return err
}
@@ -101,8 +103,10 @@ func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusU
}
switch update.Type {
case 0, 1:
+ _ = s.jobService.RenewJobLease(ctx, update.StepUuid)
_ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data)
case 4:
+ _ = s.jobService.RenewJobLease(ctx, update.StepUuid)
var progress float64
fmt.Sscanf(string(update.Data), "%f", &progress)
_ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, progress)
@@ -126,6 +130,7 @@ func (s *Server) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty
if err := s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusRunning); err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
+ _ = s.jobService.RenewJobLease(ctx, req.Id)
return &proto.Empty{}, nil
}
@@ -138,11 +143,13 @@ func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
- jobStatus := dto.JobStatusSuccess
+ var err error
if req.State != nil && req.State.Error != "" {
- jobStatus = dto.JobStatusFailure
+ err = s.jobService.HandleJobFailure(ctx, req.Id, req.State.Error)
+ } else {
+ err = s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusSuccess)
}
- if err := s.jobService.UpdateJobStatus(ctx, req.Id, jobStatus); err != nil {
+ if err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
s.untrackJobAssignment(agentID, req.Id)
@@ -165,6 +172,12 @@ func (s *Server) Log(ctx context.Context, req *proto.LogRequest) (*proto.Empty,
return &proto.Empty{}, nil
}
-func (s *Server) Extend(context.Context, *proto.ExtendRequest) (*proto.Empty, error) {
+func (s *Server) Extend(ctx context.Context, req *proto.ExtendRequest) (*proto.Empty, error) {
+ if _, _, ok := s.getAgentIDFromContext(ctx); !ok {
+ return nil, status.Error(codes.Unauthenticated, "invalid session")
+ }
+ if req != nil && req.Id != "" {
+ _ = s.jobService.RenewJobLease(ctx, req.Id)
+ }
return &proto.Empty{}, nil
}
diff --git a/internal/transport/mqtt/notification_publisher.go b/internal/transport/mqtt/notification_publisher.go
index 536d21b..d373036 100644
--- a/internal/transport/mqtt/notification_publisher.go
+++ b/internal/transport/mqtt/notification_publisher.go
@@ -25,7 +25,9 @@ func NewNotificationPublisher(client pahomqtt.Client, appLogger logger.Logger) s
type serviceNotificationNoop struct{}
-func (serviceNotificationNoop) PublishNotificationCreated(context.Context, *model.Notification) error { return nil }
+func (serviceNotificationNoop) PublishNotificationCreated(context.Context, *model.Notification) error {
+ return nil
+}
func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, notification *model.Notification) error {
if p == nil || notification == nil {
@@ -41,4 +43,3 @@ func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, no
func (p *notificationPublisher) notificationTopic(userID string) string {
return fmt.Sprintf("%s/notifications/%s", p.prefix, userID)
}
-
diff --git a/internal/workflow/agent/agent.go b/internal/workflow/agent/agent.go
new file mode 100644
index 0000000..b566145
--- /dev/null
+++ b/internal/workflow/agent/agent.go
@@ -0,0 +1,576 @@
+package agent
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ grpcpkg "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+ proto "stream.api/internal/api/proto/agent/v1"
+)
+
+type Agent struct {
+ client proto.WoodpeckerClient
+ authClient proto.WoodpeckerAuthClient
+ conn *grpcpkg.ClientConn
+ secret string
+ token string
+ capacity int
+ agentID string
+ docker *DockerExecutor
+ semaphore chan struct{}
+ wg sync.WaitGroup
+ activeJobs sync.Map
+ prevCPUTotal uint64
+ prevCPUIdle uint64
+}
+
+type JobPayload struct {
+ Image string `json:"image"`
+ Commands []string `json:"commands"`
+ Environment map[string]string `json:"environment"`
+ Action string `json:"action"`
+}
+
+func New(serverAddr, secret string, capacity int) (*Agent, error) {
+ conn, err := grpcpkg.NewClient(serverAddr, grpcpkg.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to server: %w", err)
+ }
+
+ docker, err := NewDockerExecutor()
+ if err != nil {
+ return nil, fmt.Errorf("failed to initialize docker executor: %w", err)
+ }
+
+ return &Agent{
+ client: proto.NewWoodpeckerClient(conn),
+ authClient: proto.NewWoodpeckerAuthClient(conn),
+ conn: conn,
+ secret: secret,
+ capacity: capacity,
+ docker: docker,
+ semaphore: make(chan struct{}, capacity),
+ }, nil
+}
+
+func (a *Agent) Run(ctx context.Context) error {
+ defer func() {
+ unregisterCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ _ = a.unregister(unregisterCtx)
+ _ = a.conn.Close()
+ }()
+
+ for {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ if err := a.registerWithRetry(ctx); err != nil {
+ log.Printf("Registration failed hard: %v", err)
+ return err
+ }
+
+ log.Printf("Agent started/reconnected with ID: %s, Capacity: %d", a.agentID, a.capacity)
+
+ sessionCtx, sessionCancel := context.WithCancel(ctx)
+ a.startBackgroundRoutines(sessionCtx)
+ err := a.streamJobs(sessionCtx)
+ sessionCancel()
+ a.wg.Wait()
+
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ log.Printf("Session ended: %v. Re-registering in 5s...", err)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(5 * time.Second):
+ }
+ }
+}
+
+func (a *Agent) registerWithRetry(ctx context.Context) error {
+ backoff := 1 * time.Second
+ maxBackoff := 30 * time.Second
+
+ for {
+ if err := a.register(ctx); err != nil {
+ log.Printf("Registration failed: %v. Retrying in %v...", err, backoff)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(backoff):
+ backoff *= 2
+ if backoff > maxBackoff {
+ backoff = maxBackoff
+ }
+ continue
+ }
+ }
+ return nil
+ }
+}
+
+func (a *Agent) startBackgroundRoutines(ctx context.Context) {
+ go a.cancelListener(ctx)
+ go a.submitStatusLoop(ctx)
+}
+
+func (a *Agent) cancelListener(context.Context) {}
+
+func (a *Agent) register(ctx context.Context) error {
+ var savedID string
+ if os.Getenv("FORCE_NEW_ID") != "true" {
+ var err error
+ savedID, err = a.loadAgentID()
+ if err == nil && savedID != "" {
+ log.Printf("Loaded persisted Agent ID: %s", savedID)
+ a.agentID = savedID
+ }
+ } else {
+ log.Println("Forcing new Agent ID due to FORCE_NEW_ID=true")
+ }
+
+ authResp, err := a.authClient.Auth(ctx, &proto.AuthRequest{
+ AgentToken: a.secret,
+ AgentId: a.agentID,
+ Hostname: a.getHostFingerprint(),
+ })
+ if err != nil {
+ return fmt.Errorf("auth failed: %w", err)
+ }
+ a.agentID = authResp.AgentId
+
+ if a.agentID != savedID {
+ if err := a.saveAgentID(a.agentID); err != nil {
+ log.Printf("Failed to save agent ID: %v", err)
+ } else {
+ log.Printf("Persisted Agent ID: %s", a.agentID)
+ }
+ }
+
+ a.token = authResp.AccessToken
+ mdCtx := metadata.AppendToOutgoingContext(ctx, "token", a.token)
+ hostname := a.getHostFingerprint()
+
+ _, err = a.client.RegisterAgent(mdCtx, &proto.RegisterAgentRequest{
+ Info: &proto.AgentInfo{
+ Platform: "linux/amd64",
+ Backend: "ffmpeg",
+ Version: "stream-api-agent-v1",
+ Capacity: int32(a.capacity),
+ CustomLabels: map[string]string{
+ "hostname": hostname,
+ },
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("registration failed: %w", err)
+ }
+
+ return nil
+}
+
+func (a *Agent) withToken(ctx context.Context) context.Context {
+ return metadata.AppendToOutgoingContext(ctx, "token", a.token)
+}
+
+func (a *Agent) streamJobs(ctx context.Context) error {
+ mdCtx := a.withToken(ctx)
+ hostname, err := os.Hostname()
+ if err != nil {
+ hostname = "unknown-agent"
+ }
+
+ stream, err := a.client.StreamJobs(mdCtx, &proto.StreamOptions{
+ Filter: &proto.Filter{
+ Labels: map[string]string{
+ "hostname": hostname,
+ },
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to start job stream: %w", err)
+ }
+
+ log.Println("Connected to job stream")
+
+ for {
+ select {
+ case a.semaphore <- struct{}{}:
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-stream.Context().Done():
+ return stream.Context().Err()
+ }
+
+ workflow, err := stream.Recv()
+ if err != nil {
+ <-a.semaphore
+ return fmt.Errorf("stream closed or error: %w", err)
+ }
+
+ if workflow.Cancel {
+ <-a.semaphore
+ log.Printf("Received cancellation signal for job %s", workflow.Id)
+ if found := a.CancelJob(workflow.Id); found {
+ log.Printf("Job %s cancellation triggered", workflow.Id)
+ } else {
+ log.Printf("Job %s not found in active jobs", workflow.Id)
+ }
+ continue
+ }
+
+ log.Printf("Received job from stream: %s (active: %d/%d)", workflow.Id, len(a.semaphore), a.capacity)
+ a.wg.Add(1)
+ go func(wf *proto.Workflow) {
+ defer a.wg.Done()
+ defer func() { <-a.semaphore }()
+ a.executeJob(ctx, wf)
+ }(workflow)
+ }
+}
+
+func (a *Agent) submitStatusLoop(ctx context.Context) {
+ for {
+ if err := a.runStatusStream(ctx); err != nil {
+ log.Printf("Status stream error: %v. Retrying in 5s...", err)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(5 * time.Second):
+ continue
+ }
+ }
+ return
+ }
+}
+
+func (a *Agent) runStatusStream(ctx context.Context) error {
+ mdCtx := a.withToken(ctx)
+ stream, err := a.client.SubmitStatus(mdCtx)
+ if err != nil {
+ return err
+ }
+
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ _, _ = stream.CloseAndRecv()
+ return ctx.Err()
+ case <-ticker.C:
+ cpu, ram := a.collectSystemResources()
+ data := fmt.Sprintf(`{"cpu": %.2f, "ram": %.2f}`, cpu, ram)
+ if err := stream.Send(&proto.StatusUpdate{Type: 5, Time: time.Now().Unix(), Data: []byte(data)}); err != nil {
+ return err
+ }
+ }
+ }
+}
+
+func (a *Agent) collectSystemResources() (float64, float64) {
+ var memTotal, memAvailable uint64
+
+ data, err := os.ReadFile("/proc/meminfo")
+ if err == nil {
+ lines := strings.Split(string(data), "\n")
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) < 2 {
+ continue
+ }
+ switch fields[0] {
+ case "MemTotal:":
+ memTotal, _ = strconv.ParseUint(fields[1], 10, 64)
+ case "MemAvailable:":
+ memAvailable, _ = strconv.ParseUint(fields[1], 10, 64)
+ }
+ }
+ }
+
+ usedRAM := 0.0
+ if memTotal > 0 {
+ usedRAM = float64(memTotal-memAvailable) / 1024.0
+ }
+
+ var cpuUsage float64
+ data, err = os.ReadFile("/proc/stat")
+ if err == nil {
+ lines := strings.Split(string(data), "\n")
+ for _, line := range lines {
+ if !strings.HasPrefix(line, "cpu ") {
+ continue
+ }
+ fields := strings.Fields(line)
+ if len(fields) < 5 {
+ break
+ }
+
+ var user, nice, system, idle, iowait, irq, softirq, steal uint64
+ user, _ = strconv.ParseUint(fields[1], 10, 64)
+ nice, _ = strconv.ParseUint(fields[2], 10, 64)
+ system, _ = strconv.ParseUint(fields[3], 10, 64)
+ idle, _ = strconv.ParseUint(fields[4], 10, 64)
+ if len(fields) > 5 {
+ iowait, _ = strconv.ParseUint(fields[5], 10, 64)
+ }
+ if len(fields) > 6 {
+ irq, _ = strconv.ParseUint(fields[6], 10, 64)
+ }
+ if len(fields) > 7 {
+ softirq, _ = strconv.ParseUint(fields[7], 10, 64)
+ }
+ if len(fields) > 8 {
+ steal, _ = strconv.ParseUint(fields[8], 10, 64)
+ }
+
+ currentIdle := idle + iowait
+ currentNonIdle := user + nice + system + irq + softirq + steal
+ currentTotal := currentIdle + currentNonIdle
+ totalDiff := currentTotal - a.prevCPUTotal
+ idleDiff := currentIdle - a.prevCPUIdle
+
+ if totalDiff > 0 && a.prevCPUTotal > 0 {
+ cpuUsage = float64(totalDiff-idleDiff) / float64(totalDiff) * 100.0
+ }
+
+ a.prevCPUTotal = currentTotal
+ a.prevCPUIdle = currentIdle
+ break
+ }
+ }
+
+ return cpuUsage, usedRAM
+}
+
+func (a *Agent) executeJob(ctx context.Context, workflow *proto.Workflow) {
+ log.Printf("Executing job %s", workflow.Id)
+
+ jobCtx, jobCancel := context.WithCancel(ctx)
+ defer jobCancel()
+
+ if workflow.Timeout > 0 {
+ timeoutDuration := time.Duration(workflow.Timeout) * time.Second
+ log.Printf("Job %s has timeout of %v", workflow.Id, timeoutDuration)
+ jobCtx, jobCancel = context.WithTimeout(jobCtx, timeoutDuration)
+ defer jobCancel()
+ }
+
+ a.activeJobs.Store(workflow.Id, jobCancel)
+ defer a.activeJobs.Delete(workflow.Id)
+
+ var payload JobPayload
+ if err := json.Unmarshal(workflow.Payload, &payload); err != nil {
+ log.Printf("Failed to parse payload for job %s: %v", workflow.Id, err)
+ a.reportDone(ctx, workflow.Id, fmt.Sprintf("invalid payload: %v", err))
+ return
+ }
+
+ if payload.Action != "" {
+ log.Printf("Received system command: %s", payload.Action)
+ a.reportDone(ctx, workflow.Id, "")
+
+ switch payload.Action {
+ case "restart":
+ log.Println("Restarting agent...")
+ os.Exit(0)
+ case "update":
+ log.Println("Updating agent...")
+ imageName := os.Getenv("AGENT_IMAGE")
+ if imageName == "" {
+ imageName = "stream-api-agent:latest"
+ }
+
+ if err := a.docker.SelfUpdate(context.Background(), imageName, a.agentID); err != nil {
+ log.Printf("Update failed: %v", err)
+ } else {
+ os.Exit(0)
+ }
+ }
+ return
+ }
+
+ mdCtx := a.withToken(ctx)
+ if _, err := a.client.Init(mdCtx, &proto.InitRequest{Id: workflow.Id}); err != nil {
+ log.Printf("Failed to init job %s: %v", workflow.Id, err)
+ return
+ }
+
+ log.Printf("Running container with image: %s", payload.Image)
+ done := make(chan error, 1)
+ go a.extendLoop(jobCtx, workflow.Id)
+ go func() {
+ done <- a.docker.Run(jobCtx, payload.Image, payload.Commands, payload.Environment, func(line string) {
+ progress := -1.0
+ if val, ok := parseProgress(line); ok {
+ progress = val
+ }
+
+ entries := []*proto.LogEntry{{
+ StepUuid: workflow.Id,
+ Data: []byte(line),
+ Time: time.Now().Unix(),
+ Type: 1,
+ }}
+
+ if progress >= 0 {
+ entries = append(entries, &proto.LogEntry{
+ StepUuid: workflow.Id,
+ Time: time.Now().Unix(),
+ Type: 4,
+ Data: []byte(fmt.Sprintf("%f", progress)),
+ })
+ }
+
+ if _, err := a.client.Log(mdCtx, &proto.LogRequest{LogEntries: entries}); err != nil {
+ log.Printf("Failed to send log for job %s: %v", workflow.Id, err)
+ }
+ })
+ }()
+
+ var err error
+ select {
+ case err = <-done:
+ case <-jobCtx.Done():
+ if jobCtx.Err() == context.DeadlineExceeded {
+ err = fmt.Errorf("job timeout exceeded")
+ log.Printf("Job %s timed out", workflow.Id)
+ } else {
+ err = fmt.Errorf("job cancelled")
+ log.Printf("Job %s was cancelled", workflow.Id)
+ }
+ }
+
+ if err != nil {
+ log.Printf("Job %s failed: %v", workflow.Id, err)
+ a.reportDone(ctx, workflow.Id, err.Error())
+ } else {
+ log.Printf("Job %s succeeded", workflow.Id)
+ a.reportDone(ctx, workflow.Id, "")
+ }
+}
+
+func (a *Agent) CancelJob(jobID string) bool {
+ if cancelFunc, ok := a.activeJobs.Load(jobID); ok {
+ log.Printf("Cancelling job %s", jobID)
+ cancelFunc.(context.CancelFunc)()
+ return true
+ }
+ return false
+}
+
+func (a *Agent) extendLoop(ctx context.Context, jobID string) {
+ ticker := time.NewTicker(5 * time.Minute)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ mdCtx := a.withToken(ctx)
+ if _, err := a.client.Extend(mdCtx, &proto.ExtendRequest{Id: jobID}); err != nil {
+ log.Printf("Failed to extend lease for job %s: %v", jobID, err)
+ }
+ }
+ }
+}
+
+func (a *Agent) reportDone(_ context.Context, id string, errStr string) {
+ state := &proto.WorkflowState{Finished: time.Now().Unix()}
+ if errStr != "" {
+ state.Error = errStr
+ }
+
+ reportCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ mdCtx := a.withToken(reportCtx)
+ _, err := a.client.Done(mdCtx, &proto.DoneRequest{Id: id, State: state})
+ if err != nil {
+ log.Printf("Failed to report Done for job %s: %v", id, err)
+ }
+}
+
+func (a *Agent) unregister(ctx context.Context) error {
+ if a.token == "" {
+ return nil
+ }
+
+ mdCtx := a.withToken(ctx)
+ _, err := a.client.UnregisterAgent(mdCtx, &proto.Empty{})
+ if err != nil {
+ return fmt.Errorf("failed to unregister agent: %w", err)
+ }
+ return nil
+}
+
+const (
+ AgentIDFile = "/data/agent_id"
+ HostnameFile = "/host_hostname"
+)
+
+type AgentIdentity struct {
+ ID string `json:"id"`
+ Fingerprint string `json:"fingerprint"`
+}
+
+func (a *Agent) getHostFingerprint() string {
+ data, err := os.ReadFile(HostnameFile)
+ if err == nil {
+ return strings.TrimSpace(string(data))
+ }
+ hostname, _ := os.Hostname()
+ return hostname
+}
+
+func (a *Agent) loadAgentID() (string, error) {
+ data, err := os.ReadFile(AgentIDFile)
+ if err != nil {
+ return "", err
+ }
+
+ var identity AgentIdentity
+ if err := json.Unmarshal(data, &identity); err == nil {
+ currentFP := a.getHostFingerprint()
+ if identity.Fingerprint != "" && identity.Fingerprint != currentFP {
+ log.Printf("Environment changed (Hostname mismatch: saved=%s, current=%s). Resetting Agent ID.", identity.Fingerprint, currentFP)
+ return "", fmt.Errorf("environment changed")
+ }
+ return identity.ID, nil
+ }
+
+ id := strings.TrimSpace(string(data))
+ if id == "" {
+ return "", fmt.Errorf("empty ID")
+ }
+ return id, nil
+}
+
+func (a *Agent) saveAgentID(id string) error {
+ if err := os.MkdirAll("/data", 0755); err != nil {
+ return err
+ }
+
+ identity := AgentIdentity{ID: id, Fingerprint: a.getHostFingerprint()}
+ data, err := json.Marshal(identity)
+ if err != nil {
+ return err
+ }
+
+ return os.WriteFile(AgentIDFile, data, 0644)
+}
diff --git a/internal/workflow/agent/docker.go b/internal/workflow/agent/docker.go
new file mode 100644
index 0000000..c5cd404
--- /dev/null
+++ b/internal/workflow/agent/docker.go
@@ -0,0 +1,154 @@
+package agent
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/client"
+)
+
+type DockerExecutor struct {
+ cli *client.Client
+}
+
+func NewDockerExecutor() (*DockerExecutor, error) {
+ cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
+ if err != nil {
+ return nil, err
+ }
+ return &DockerExecutor{cli: cli}, nil
+}
+
+func (d *DockerExecutor) Run(ctx context.Context, imageName string, commands []string, env map[string]string, logCallback func(string)) error {
+ reader, err := d.cli.ImagePull(ctx, imageName, image.PullOptions{})
+ if err != nil {
+ log.Printf("Warning: Failed to pull image %s (might exist locally): %v", imageName, err)
+ } else {
+ _, _ = io.Copy(io.Discard, reader)
+ _ = reader.Close()
+ }
+
+ envSlice := make([]string, 0, len(env))
+ for k, v := range env {
+ envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v))
+ }
+
+ script := strings.Join(commands, " && ")
+ if len(commands) == 0 {
+ script = "echo 'No commands to execute'"
+ }
+
+ cmd := []string{"/bin/sh", "-c", script}
+
+ resp, err := d.cli.ContainerCreate(ctx, &container.Config{
+ Image: imageName,
+ Entrypoint: cmd,
+ Cmd: nil,
+ Env: envSlice,
+ Tty: true,
+ }, nil, nil, nil, "")
+ if err != nil {
+ return fmt.Errorf("failed to create container: %w", err)
+ }
+
+ containerID := resp.ID
+ defer d.cleanup(context.Background(), containerID)
+
+ if err := d.cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
+ return fmt.Errorf("failed to start container: %w", err)
+ }
+
+ var wg sync.WaitGroup
+
+ out, err := d.cli.ContainerLogs(ctx, containerID, container.LogsOptions{
+ ShowStdout: true,
+ ShowStderr: true,
+ Follow: true,
+ })
+ if err != nil {
+ log.Printf("Failed to get logs: %v", err)
+ } else {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer out.Close()
+ scanner := bufio.NewScanner(out)
+ for scanner.Scan() {
+ logCallback(scanner.Text())
+ }
+ }()
+ }
+
+ statusCh, errCh := d.cli.ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
+ select {
+ case err := <-errCh:
+ if err != nil {
+ return fmt.Errorf("error waiting for container: %w", err)
+ }
+ case status := <-statusCh:
+ if status.StatusCode != 0 {
+ wg.Wait()
+ return fmt.Errorf("container exited with code %d", status.StatusCode)
+ }
+ }
+
+ wg.Wait()
+ return nil
+}
+
+func (d *DockerExecutor) cleanup(ctx context.Context, containerID string) {
+ _ = d.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true})
+}
+
+func (d *DockerExecutor) SelfUpdate(ctx context.Context, imageTag string, agentID string) error {
+ log.Printf("Initiating self-update using Watchtower...")
+
+ containerID, err := os.Hostname()
+ if err != nil {
+ return fmt.Errorf("failed to get hostname (container ID): %w", err)
+ }
+ log.Printf("Current Container ID: %s", containerID)
+
+ watchtowerImage := "containrrr/watchtower:latest"
+ reader, err := d.cli.ImagePull(ctx, watchtowerImage, image.PullOptions{})
+ if err != nil {
+ log.Printf("Failed to pull watchtower: %v", err)
+ return fmt.Errorf("failed to pull watchtower: %w", err)
+ }
+ _, _ = io.Copy(io.Discard, reader)
+ _ = reader.Close()
+
+ hostSock := os.Getenv("HOST_DOCKER_SOCK")
+ if hostSock == "" {
+ hostSock = "/var/run/docker.sock"
+ }
+
+ cmd := []string{"/watchtower", "--run-once", "--cleanup", "--debug", containerID}
+
+ resp, err := d.cli.ContainerCreate(ctx, &container.Config{
+ Image: watchtowerImage,
+ Cmd: cmd,
+ }, &container.HostConfig{
+ Binds: []string{fmt.Sprintf("%s:/var/run/docker.sock", hostSock)},
+ }, nil, nil, fmt.Sprintf("watchtower-updater-%s-%d", agentID, time.Now().Unix()))
+ if err != nil {
+ return fmt.Errorf("failed to create watchtower container: %w", err)
+ }
+
+ if err := d.cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
+ return fmt.Errorf("failed to start watchtower: %w", err)
+ }
+
+ log.Printf("Watchtower started with ID: %s. Monitoring...", resp.ID)
+ time.Sleep(2 * time.Second)
+ return nil
+}
diff --git a/internal/workflow/agent/parser.go b/internal/workflow/agent/parser.go
new file mode 100644
index 0000000..39d62b5
--- /dev/null
+++ b/internal/workflow/agent/parser.go
@@ -0,0 +1,26 @@
+package agent
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+func parseProgress(line string) (float64, bool) {
+ if !strings.Contains(line, "out_time_us=") {
+ return 0, false
+ }
+
+ re := regexp.MustCompile(`out_time_us=(\d+)`)
+ matches := re.FindStringSubmatch(line)
+ if len(matches) <= 1 {
+ return 0, false
+ }
+
+ us, err := strconv.ParseInt(matches[1], 10, 64)
+ if err != nil {
+ return 0, false
+ }
+
+ return float64(us) / 1000000.0, true
+}
diff --git a/internal/workflow/agent/parser_test.go b/internal/workflow/agent/parser_test.go
new file mode 100644
index 0000000..6e3c9b9
--- /dev/null
+++ b/internal/workflow/agent/parser_test.go
@@ -0,0 +1,43 @@
+package agent
+
+import "testing"
+
+func TestParseProgress(t *testing.T) {
+ tests := []struct {
+ name string
+ line string
+ expected float64
+ ok bool
+ }{
+ {
+ name: "valid ffmpeg output",
+ line: "frame= 171 fps=0.0 q=-1.0 size= 1024kB time=00:00:06.84 bitrate=1225.6kbits/s speed=13.6x out_time_us=1234567",
+ expected: 1.234567,
+ ok: true,
+ },
+ {
+ name: "line without out_time_us",
+ line: "frame= 171 fps=0.0 q=-1.0 size= 1024kB time=00:00:06.84",
+ expected: 0,
+ ok: false,
+ },
+ {
+ name: "invalid out_time_us value",
+ line: "out_time_us=invalid",
+ expected: 0,
+ ok: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, ok := parseProgress(tt.line)
+ if ok != tt.ok {
+ t.Errorf("parseProgress() ok = %v, want %v", ok, tt.ok)
+ }
+ if got != tt.expected {
+ t.Errorf("parseProgress() got = %v, want %v", got, tt.expected)
+ }
+ })
+ }
+}
diff --git a/internal/workflow/render/workflow.go b/internal/workflow/render/workflow.go
index c32a8a2..f913b2a 100644
--- a/internal/workflow/render/workflow.go
+++ b/internal/workflow/render/workflow.go
@@ -28,6 +28,10 @@ type JobService interface {
CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*model.Job, error)
CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, error)
+ ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error)
+ GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error)
+ RetryDLQ(ctx context.Context, id string) (*model.Job, error)
+ RemoveDLQ(ctx context.Context, id string) error
}
type Workflow struct {
@@ -187,6 +191,34 @@ func (w *Workflow) RetryJob(ctx context.Context, id string) (*model.Job, error)
return w.jobService.RetryJob(ctx, id)
}
+func (w *Workflow) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) {
+ if w == nil || w.jobService == nil {
+ return nil, 0, ErrJobServiceUnavailable
+ }
+ return w.jobService.ListDLQ(ctx, offset, limit)
+}
+
+func (w *Workflow) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) {
+ if w == nil || w.jobService == nil {
+ return nil, ErrJobServiceUnavailable
+ }
+ return w.jobService.GetDLQ(ctx, id)
+}
+
+func (w *Workflow) RetryDLQ(ctx context.Context, id string) (*model.Job, error) {
+ if w == nil || w.jobService == nil {
+ return nil, ErrJobServiceUnavailable
+ }
+ return w.jobService.RetryDLQ(ctx, id)
+}
+
+func (w *Workflow) RemoveDLQ(ctx context.Context, id string) error {
+ if w == nil || w.jobService == nil {
+ return ErrJobServiceUnavailable
+ }
+ return w.jobService.RemoveDLQ(ctx, id)
+}
+
func buildJobPayload(videoID, userID, videoURL, format string) ([]byte, error) {
return json.Marshal(map[string]any{
"video_id": videoID,
diff --git a/proto/app/v1/admin.proto b/proto/app/v1/admin.proto
index 8e3e5e5..db82940 100644
--- a/proto/app/v1/admin.proto
+++ b/proto/app/v1/admin.proto
@@ -49,6 +49,10 @@ service Admin {
rpc CreateAdminJob(CreateAdminJobRequest) returns (CreateAdminJobResponse);
rpc CancelAdminJob(CancelAdminJobRequest) returns (CancelAdminJobResponse);
rpc RetryAdminJob(RetryAdminJobRequest) returns (RetryAdminJobResponse);
+ rpc ListAdminDlqJobs(ListAdminDlqJobsRequest) returns (ListAdminDlqJobsResponse);
+ rpc GetAdminDlqJob(GetAdminDlqJobRequest) returns (GetAdminDlqJobResponse);
+ rpc RetryAdminDlqJob(RetryAdminDlqJobRequest) returns (RetryAdminDlqJobResponse);
+ rpc RemoveAdminDlqJob(RemoveAdminDlqJobRequest) returns (RemoveAdminDlqJobResponse);
rpc ListAdminAgents(ListAdminAgentsRequest) returns (ListAdminAgentsResponse);
rpc RestartAdminAgent(RestartAdminAgentRequest) returns (AdminAgentCommandResponse);
rpc UpdateAdminAgent(UpdateAdminAgentRequest) returns (AdminAgentCommandResponse);
@@ -533,6 +537,43 @@ message RetryAdminJobResponse {
AdminJob job = 1;
}
+message ListAdminDlqJobsRequest {
+ int32 offset = 1;
+ int32 limit = 2;
+}
+
+message ListAdminDlqJobsResponse {
+ repeated AdminDlqEntry items = 1;
+ int64 total = 2;
+ int32 offset = 3;
+ int32 limit = 4;
+}
+
+message GetAdminDlqJobRequest {
+ string id = 1;
+}
+
+message GetAdminDlqJobResponse {
+ AdminDlqEntry item = 1;
+}
+
+message RetryAdminDlqJobRequest {
+ string id = 1;
+}
+
+message RetryAdminDlqJobResponse {
+ AdminJob job = 1;
+}
+
+message RemoveAdminDlqJobRequest {
+ string id = 1;
+}
+
+message RemoveAdminDlqJobResponse {
+ string status = 1;
+ string job_id = 2;
+}
+
message ListAdminAgentsRequest {}
message ListAdminAgentsResponse {
diff --git a/proto/app/v1/common.proto b/proto/app/v1/common.proto
index a5293d9..617247e 100644
--- a/proto/app/v1/common.proto
+++ b/proto/app/v1/common.proto
@@ -392,3 +392,10 @@ message AdminAgent {
google.protobuf.Timestamp created_at = 11;
google.protobuf.Timestamp updated_at = 12;
}
+
+message AdminDlqEntry {
+ AdminJob job = 1;
+ google.protobuf.Timestamp failure_time = 2;
+ string reason = 3;
+ int32 retry_count = 4;
+}
diff --git a/proto/app/v1/video_metadata.proto b/proto/app/v1/video_metadata.proto
new file mode 100644
index 0000000..6f44947
--- /dev/null
+++ b/proto/app/v1/video_metadata.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+
+package stream.app.v1;
+
+option go_package = "stream.api/internal/gen/proto/app/v1;appv1";
+
+import "app/v1/common.proto";
+
+service VideoMetadata {
+ rpc GetVideoMetadata(GetVideoMetadataRequest) returns (GetVideoMetadataResponse);
+}
+
+message GetVideoMetadataRequest {
+ string video_id = 1;
+}
+
+message GetVideoMetadataResponse {
+ Video video = 1;
+ PlayerConfig default_player_config = 2;
+ AdTemplate ad_template = 3;
+ PopupAd active_popup_ad = 4;
+ repeated Domain domains = 5;
+}
diff --git a/MIGRATION_GUIDE.md b/script/MIGRATION_GUIDE.md
similarity index 100%
rename from MIGRATION_GUIDE.md
rename to script/MIGRATION_GUIDE.md
diff --git a/script/create_database.sql b/script/create_database.sql
new file mode 100644
index 0000000..23928be
--- /dev/null
+++ b/script/create_database.sql
@@ -0,0 +1,376 @@
+-- DROP SCHEMA public;
+
+CREATE SCHEMA public AUTHORIZATION pg_database_owner;
+
+COMMENT ON SCHEMA public IS 'standard public schema';
+-- public.ad_templates definition
+
+-- Drop table
+
+-- DROP TABLE public.ad_templates;
+
+CREATE TABLE public.ad_templates (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ "name" text NOT NULL,
+ description text NULL,
+ vast_tag_url text NOT NULL,
+ ad_format varchar(50) DEFAULT 'pre-roll'::character varying NOT NULL,
+ duration int8 NULL,
+ is_active bool DEFAULT true NOT NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ is_default bool DEFAULT false NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT ad_templates_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_ad_templates_user_id ON public.ad_templates USING btree (user_id);
+
+
+-- public.domains definition
+
+-- Drop table
+
+-- DROP TABLE public.domains;
+
+CREATE TABLE public.domains (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ "name" text NOT NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT domains_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_domains_user_id ON public.domains USING btree (user_id);
+
+
+-- public.jobs definition
+
+-- Drop table
+
+-- DROP TABLE public.jobs;
+
+CREATE TABLE public.jobs (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ status text NULL,
+ priority int8 DEFAULT 0 NULL,
+ input_url text NULL,
+ output_url text NULL,
+ total_duration int8 NULL,
+ current_time int8 NULL,
+ progress numeric NULL,
+ agent_id int8 NULL,
+ logs text NULL,
+ config text NULL,
+ cancelled bool DEFAULT false NULL,
+ retry_count int8 DEFAULT 0 NULL,
+ max_retries int8 DEFAULT 3 NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ "version" int8 NULL,
+ video_id uuid NULL,
+ user_id uuid NULL,
+ time_limit int8 DEFAULT 3600000 NULL,
+ CONSTRAINT jobs_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_jobs_priority ON public.jobs USING btree (priority);
+
+
+-- public.notifications definition
+
+-- Drop table
+
+-- DROP TABLE public.notifications;
+
+CREATE TABLE public.notifications (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ "type" varchar(50) NOT NULL,
+ title text NOT NULL,
+ message text NOT NULL,
+ metadata text NULL,
+ action_url text NULL,
+ action_label text NULL,
+ is_read bool DEFAULT false NOT NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT notifications_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_notifications_user_id ON public.notifications USING btree (user_id);
+
+
+-- public."plan" definition
+
+-- Drop table
+
+-- DROP TABLE public."plan";
+
+CREATE TABLE public."plan" (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ "name" text NOT NULL,
+ description text NULL,
+ price numeric(65, 30) NOT NULL,
+ "cycle" varchar(20) NOT NULL,
+ storage_limit int8 NOT NULL,
+ upload_limit int4 NOT NULL,
+ duration_limit int4 NOT NULL,
+ quality_limit text NOT NULL,
+ features _text NULL,
+ is_active bool DEFAULT true NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT plan_pkey PRIMARY KEY (id)
+);
+
+
+-- public.plan_subscriptions definition
+
+-- Drop table
+
+-- DROP TABLE public.plan_subscriptions;
+
+CREATE TABLE public.plan_subscriptions (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ payment_id uuid NOT NULL,
+ plan_id uuid NOT NULL,
+ term_months int4 NOT NULL,
+ payment_method varchar(20) NOT NULL,
+ wallet_amount numeric(65, 30) NOT NULL,
+ topup_amount numeric(65, 30) NOT NULL,
+ started_at timestamptz NOT NULL,
+ expires_at timestamptz NOT NULL,
+ reminder_7d_sent_at timestamptz NULL,
+ reminder_3d_sent_at timestamptz NULL,
+ reminder_1d_sent_at timestamptz NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT plan_subscriptions_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_plan_subscriptions_expires_at ON public.plan_subscriptions USING btree (expires_at);
+CREATE INDEX idx_plan_subscriptions_payment_id ON public.plan_subscriptions USING btree (payment_id);
+CREATE INDEX idx_plan_subscriptions_plan_id ON public.plan_subscriptions USING btree (plan_id);
+CREATE INDEX idx_plan_subscriptions_user_id ON public.plan_subscriptions USING btree (user_id);
+
+
+-- public.user_preferences definition
+
+-- Drop table
+
+-- DROP TABLE public.user_preferences;
+
+CREATE TABLE public.user_preferences (
+ user_id uuid NOT NULL,
+ "language" text DEFAULT 'en'::text NOT NULL,
+ locale text DEFAULT 'en'::text NOT NULL,
+ email_notifications bool DEFAULT true NOT NULL,
+ push_notifications bool DEFAULT true NOT NULL,
+ marketing_notifications bool DEFAULT false NOT NULL,
+ telegram_notifications bool DEFAULT false NOT NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT user_preferences_pkey PRIMARY KEY (user_id)
+);
+
+
+-- public.wallet_transactions definition
+
+-- Drop table
+
+-- DROP TABLE public.wallet_transactions;
+
+CREATE TABLE public.wallet_transactions (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ "type" varchar(50) NOT NULL,
+ amount numeric(65, 30) NOT NULL,
+ currency text DEFAULT 'USD'::text NOT NULL,
+ note text NULL,
+ created_at timestamptz NULL,
+ updated_at timestamptz NULL,
+ payment_id uuid NULL,
+ plan_id uuid NULL,
+ term_months int4 NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT wallet_transactions_pkey PRIMARY KEY (id)
+);
+CREATE INDEX idx_wallet_transactions_payment_id ON public.wallet_transactions USING btree (payment_id);
+CREATE INDEX idx_wallet_transactions_plan_id ON public.wallet_transactions USING btree (plan_id);
+CREATE INDEX idx_wallet_transactions_user_id ON public.wallet_transactions USING btree (user_id);
+
+
+-- public."user" definition
+
+-- Drop table
+
+-- DROP TABLE public."user";
+
+CREATE TABLE public."user" (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ email text NOT NULL,
+ "password" text NULL,
+ username text NULL,
+ avatar text NULL,
+ "role" varchar(20) DEFAULT 'USER'::character varying NOT NULL,
+ google_id text NULL,
+ storage_used int8 DEFAULT 0 NOT NULL,
+ plan_id uuid NULL,
+ created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at timestamp(3) NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ telegram_id varchar NULL,
+ referred_by_user_id uuid NULL,
+ referral_eligible bool DEFAULT true NOT NULL,
+ referral_reward_bps int4 NULL,
+ referral_reward_granted_at timestamptz NULL,
+ referral_reward_payment_id uuid NULL,
+ referral_reward_amount numeric(65, 30) NULL,
+ CONSTRAINT user_pkey PRIMARY KEY (id),
+ CONSTRAINT user_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public."plan"(id) ON DELETE SET NULL ON UPDATE CASCADE,
+ CONSTRAINT user_referred_by_user_id_fkey FOREIGN KEY (referred_by_user_id) REFERENCES public."user"(id) ON DELETE SET NULL
+);
+CREATE INDEX idx_user_referred_by_user_id ON public."user" USING btree (referred_by_user_id);
+CREATE UNIQUE INDEX user_email_key ON public."user" USING btree (email);
+CREATE UNIQUE INDEX user_google_id_key ON public."user" USING btree (google_id);
+
+
+-- public.video definition
+
+-- Drop table
+
+-- DROP TABLE public.video;
+
+CREATE TABLE public.video (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ "name" text NOT NULL,
+ title text NOT NULL,
+ description text NULL,
+ url text NOT NULL,
+ thumbnail text NULL,
+ hls_token text NULL,
+ hls_path text NULL,
+ duration int4 NOT NULL,
+ "size" int8 NOT NULL,
+ storage_type varchar(20) DEFAULT 'tiktok_avatar'::character varying NOT NULL,
+ format text NOT NULL,
+ status varchar(20) DEFAULT 'PUBLIC'::character varying NOT NULL,
+ processing_status varchar(20) DEFAULT 'PENDING'::character varying NOT NULL,
+ "views" int4 DEFAULT 0 NOT NULL,
+ user_id uuid NOT NULL,
+ created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at timestamp(3) NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ ad_id uuid NULL,
+ metadata jsonb NULL,
+ CONSTRAINT video_pkey PRIMARY KEY (id),
+ CONSTRAINT video_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+
+-- public.payment definition
+
+-- Drop table
+
+-- DROP TABLE public.payment;
+
+CREATE TABLE public.payment (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ user_id uuid NOT NULL,
+ plan_id uuid NULL,
+ amount numeric(65, 30) NOT NULL,
+ currency text DEFAULT 'USD'::text NOT NULL,
+ status varchar(20) DEFAULT 'PENDING'::character varying NOT NULL,
+ provider varchar(20) DEFAULT 'STRIPE'::character varying NOT NULL,
+ transaction_id text NULL,
+ created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at timestamp(3) NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT payment_pkey PRIMARY KEY (id),
+ CONSTRAINT payment_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public."plan"(id) ON DELETE SET NULL ON UPDATE CASCADE,
+ CONSTRAINT payment_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+
+-- public.player_configs definition
+
+-- Drop table
+
+-- DROP TABLE public.player_configs;
+
+CREATE TABLE public.player_configs (
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
+ user_id uuid NOT NULL,
+ "name" text NOT NULL,
+ description text NULL,
+ autoplay bool DEFAULT false NOT NULL,
+ "loop" bool DEFAULT false NOT NULL,
+ muted bool DEFAULT false NOT NULL,
+ show_controls bool DEFAULT true NOT NULL,
+ pip bool DEFAULT true NOT NULL,
+ airplay bool DEFAULT true NOT NULL,
+ chromecast bool DEFAULT true NOT NULL,
+ is_active bool DEFAULT true NOT NULL,
+ is_default bool DEFAULT false NOT NULL,
+ created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at timestamp(3) NOT NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ encrytion_m3u8 bool DEFAULT true NOT NULL,
+ logo_url varchar(500) NULL,
+ CONSTRAINT player_configs_pkey PRIMARY KEY (id),
+ CONSTRAINT player_configs_url_check CHECK (((logo_url)::text ~* '^https?://'::text)),
+ CONSTRAINT player_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE
+);
+CREATE INDEX idx_player_configs_is_default ON public.player_configs USING btree (is_default);
+CREATE UNIQUE INDEX idx_player_configs_one_default_per_user ON public.player_configs USING btree (user_id) WHERE (is_default = true);
+CREATE INDEX idx_player_configs_user_default ON public.player_configs USING btree (user_id, is_default);
+CREATE INDEX idx_player_configs_user_id ON public.player_configs USING btree (user_id);
+
+-- Table Triggers
+
+create trigger trg_update_player_configs before
+update
+ on
+ public.player_configs for each row execute function update_player_configs_updated_at();
+
+
+-- public.popup_ads definition
+
+-- Drop table
+
+-- DROP TABLE public.popup_ads;
+
+CREATE TABLE public.popup_ads (
+ id uuid NOT NULL,
+ user_id uuid NOT NULL,
+ "type" varchar(20) NOT NULL,
+ "label" text NOT NULL,
+ value text NOT NULL,
+ is_active bool DEFAULT true NOT NULL,
+ max_triggers_per_session int4 DEFAULT 3 NOT NULL,
+ created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
+ updated_at timestamptz NULL,
+ "version" int8 DEFAULT 1 NOT NULL,
+ CONSTRAINT popup_ads_pkey PRIMARY KEY (id),
+ CONSTRAINT popup_ads_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE
+);
+CREATE INDEX idx_popup_ads_user_active ON public.popup_ads USING btree (user_id, is_active);
+CREATE INDEX idx_popup_ads_user_id ON public.popup_ads USING btree (user_id);
+
+
+
+-- DROP FUNCTION public.update_player_configs_updated_at();
+
+CREATE OR REPLACE FUNCTION public.update_player_configs_updated_at()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ NEW.version = OLD.version + 1;
+ RETURN NEW;
+END;
+$function$
+;
\ No newline at end of file
diff --git a/full_player_configs_migration.sql b/script/full_player_configs_migration.sql
similarity index 100%
rename from full_player_configs_migration.sql
rename to script/full_player_configs_migration.sql
diff --git a/install_player_configs.sql b/script/install_player_configs.sql
similarity index 100%
rename from install_player_configs.sql
rename to script/install_player_configs.sql
diff --git a/migrate_player_configs.sh b/script/migrate_player_configs.sh
similarity index 100%
rename from migrate_player_configs.sh
rename to script/migrate_player_configs.sh
diff --git a/run_migration.sh b/script/run_migration.sh
similarity index 100%
rename from run_migration.sh
rename to script/run_migration.sh