Files
silo-server/internal/chapterthumbs/remote.go
T

127 lines
3.9 KiB
Go

package chapterthumbs
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/Silo-Server/silo-server/internal/nodepool"
)
const (
chapterThumbnailExecutionSetting = "playback.chapter_thumbnail_execution"
chapterThumbnailExecutionLocal = "local"
chapterThumbnailExecutionPreferTranscode = "prefer_transcode_nodes"
chapterThumbnailExecutionTranscodeOnly = "transcode_nodes_only"
chapterThumbnailNodeCapacitySetting = "playback.chapter_thumbnail_node_capacity"
chapterThumbnailNodeUnavailableReason = "transcode_node_unavailable"
chapterThumbnailNodeCapacityExhaustedReason = "transcode_node_capacity_exhausted"
authJWTSecretSetting = "auth.jwt_secret"
)
type RemoteExtractRequest struct {
InputPath string `json:"input_path"`
SeekSeconds float64 `json:"seek_seconds"`
ToneMap bool `json:"tone_map"`
}
type RemoteExtractErrorResponse struct {
Reason string `json:"reason"`
Error string `json:"error,omitempty"`
}
type remoteFrameExtractor interface {
ExtractFrame(ctx context.Context, node *nodepool.Node, jwtSecret string, req RemoteExtractRequest) ([]byte, string, error)
}
type httpRemoteFrameExtractor struct {
client *http.Client
}
func (e *httpRemoteFrameExtractor) ExtractFrame(
ctx context.Context,
node *nodepool.Node,
jwtSecret string,
req RemoteExtractRequest,
) ([]byte, string, error) {
if node == nil {
return nil, chapterThumbnailNodeUnavailableReason, fmt.Errorf("chapter thumbnail remote extract: missing node")
}
if strings.TrimSpace(jwtSecret) == "" {
return nil, chapterThumbnailNodeUnavailableReason, fmt.Errorf("chapter thumbnail remote extract: missing jwt secret")
}
body, err := json.Marshal(req)
if err != nil {
return nil, "chapter_extract_failed", fmt.Errorf("chapter thumbnail remote extract: marshal request: %w", err)
}
timeout := remoteExtractTimeout(req.ToneMap)
requestCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
httpReq, err := http.NewRequestWithContext(requestCtx, http.MethodPost, node.URL+"/chapter-thumbnails/extract", bytes.NewReader(body))
if err != nil {
return nil, chapterThumbnailNodeUnavailableReason, fmt.Errorf("chapter thumbnail remote extract: build request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+jwtSecret)
client := e.client
if client == nil {
client = &http.Client{Timeout: timeout}
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, chapterThumbnailNodeUnavailableReason, fmt.Errorf("chapter thumbnail remote extract: request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
reason := chapterThumbnailNodeUnavailableReason
message := fmt.Sprintf("node returned status %d", resp.StatusCode)
var payload RemoteExtractErrorResponse
if decodeErr := json.NewDecoder(resp.Body).Decode(&payload); decodeErr == nil {
if payload.Reason != "" {
reason = payload.Reason
}
if payload.Error != "" {
message = payload.Error
}
}
return nil, reason, fmt.Errorf("chapter thumbnail remote extract: %s", message)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, chapterThumbnailNodeUnavailableReason, fmt.Errorf("chapter thumbnail remote extract: read response: %w", err)
}
if len(data) == 0 {
return nil, "chapter_extract_failed", fmt.Errorf("chapter thumbnail remote extract: empty response")
}
return data, "", nil
}
func remoteExtractTimeout(toneMap bool) time.Duration {
timeout := extractTimeoutForAttempt(true, toneMap)
if !toneMap {
timeout += extractTimeoutForAttempt(false, false)
}
return timeout + 3*time.Second
}
func isInfrastructureRemoteFailure(reason string) bool {
switch reason {
case chapterThumbnailNodeUnavailableReason, chapterThumbnailNodeCapacityExhaustedReason:
return true
default:
return false
}
}