127 lines
3.9 KiB
Go
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
|
|
}
|
|
}
|