feat: add preview sprite backend
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -203,6 +204,99 @@ func (h *Handler) HandleProxySubtitle(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.proxyUpstream(w, r, targetURL, r.URL.Query().Get("r"))
|
h.proxyUpstream(w, r, targetURL, r.URL.Query().Get("r"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandleProxyPreviewMap(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
malIDText := strings.TrimSpace(r.URL.Query().Get("mal_id"))
|
||||||
|
malID, err := strconv.Atoi(malIDText)
|
||||||
|
if err != nil || malID <= 0 {
|
||||||
|
http.Error(w, "invalid mal id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
episode := strings.TrimSpace(r.URL.Query().Get("ep"))
|
||||||
|
if episode == "" {
|
||||||
|
episode = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := normalizeMode(r.URL.Query().Get("mode"))
|
||||||
|
if mode == "" {
|
||||||
|
mode = "dub"
|
||||||
|
}
|
||||||
|
|
||||||
|
source := strings.TrimSpace(r.URL.Query().Get("u"))
|
||||||
|
if source == "" {
|
||||||
|
http.Error(w, "missing target url", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
referer := strings.TrimSpace(r.URL.Query().Get("r"))
|
||||||
|
duration := 0.0
|
||||||
|
durationText := strings.TrimSpace(r.URL.Query().Get("d"))
|
||||||
|
if durationText != "" {
|
||||||
|
parsedDuration, parseErr := strconv.ParseFloat(durationText, 64)
|
||||||
|
if parseErr != nil || parsedDuration <= 0 {
|
||||||
|
http.Error(w, "invalid duration", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
duration = parsedDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
mapData, previewKey, previewErr := h.svc.EnsurePreviewMap(r.Context(), PreviewRequest{
|
||||||
|
MalID: malID,
|
||||||
|
Episode: episode,
|
||||||
|
Mode: mode,
|
||||||
|
Source: source,
|
||||||
|
Referer: referer,
|
||||||
|
Duration: duration,
|
||||||
|
})
|
||||||
|
if previewErr != nil {
|
||||||
|
log.Printf("preview map error mal_id=%d ep=%s mode=%s: %v", malID, episode, mode, previewErr)
|
||||||
|
http.Error(w, "failed to generate preview map", http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteURL := "/watch/proxy/preview-sprite?k=" + url.QueryEscape(previewKey)
|
||||||
|
response := struct {
|
||||||
|
SpriteURL string `json:"sprite_url"`
|
||||||
|
Map PreviewMap `json:"map"`
|
||||||
|
}{
|
||||||
|
SpriteURL: spriteURL,
|
||||||
|
Map: mapData,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
log.Printf("preview map encode error mal_id=%d ep=%s mode=%s: %v", malID, episode, mode, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandleProxyPreviewSprite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewKey := strings.TrimSpace(r.URL.Query().Get("k"))
|
||||||
|
spritePath := h.svc.PreviewSpritePath(previewKey)
|
||||||
|
if spritePath == "" {
|
||||||
|
http.Error(w, "invalid preview key", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(spritePath); err != nil {
|
||||||
|
http.Error(w, "preview sprite not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
|
http.ServeFile(w, r, spritePath)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) proxyUpstream(w http.ResponseWriter, r *http.Request, targetURL string, referer string) {
|
func (h *Handler) proxyUpstream(w http.ResponseWriter, r *http.Request, targetURL string, referer string) {
|
||||||
parsed, err := url.Parse(targetURL)
|
parsed, err := url.Parse(targetURL)
|
||||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||||
|
|||||||
400
internal/features/playback/preview_service.go
Normal file
400
internal/features/playback/preview_service.go
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
previewFrameWidth = 160
|
||||||
|
previewFrameHeight = 90
|
||||||
|
previewGridColumns = 10
|
||||||
|
previewGridRows = 10
|
||||||
|
previewFrameInterval = 10
|
||||||
|
previewFrameLimit = previewGridColumns * previewGridRows
|
||||||
|
previewGenerationLimit = 120 * time.Second
|
||||||
|
previewManifestName = "map.json"
|
||||||
|
spriteFileName = "sprite.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ffprobeFormat struct {
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ffprobeResult struct {
|
||||||
|
Format ffprobeFormat `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPreviewRootDir() string {
|
||||||
|
return filepath.Join(os.TempDir(), "mal-preview-cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) EnsurePreviewMap(ctx context.Context, req PreviewRequest) (PreviewMap, string, error) {
|
||||||
|
if strings.TrimSpace(req.Source) == "" {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("missing preview source")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSource, err := url.Parse(req.Source)
|
||||||
|
if err != nil {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("invalid preview source")
|
||||||
|
}
|
||||||
|
if parsedSource.Scheme != "http" && parsedSource.Scheme != "https" {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("invalid preview source scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedEpisode := strings.TrimSpace(req.Episode)
|
||||||
|
if normalizedEpisode == "" {
|
||||||
|
normalizedEpisode = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedMode := normalizeMode(req.Mode)
|
||||||
|
if normalizedMode == "" {
|
||||||
|
normalizedMode = "dub"
|
||||||
|
}
|
||||||
|
|
||||||
|
previewHash := hashPreviewIdentity(req.MalID, normalizedEpisode, normalizedMode, req.Source, req.Referer)
|
||||||
|
previewKey := fmt.Sprintf("%d-%s", req.MalID, previewHash)
|
||||||
|
previewDir := filepath.Join(s.previewRoot, previewKey)
|
||||||
|
manifestPath := filepath.Join(previewDir, previewManifestName)
|
||||||
|
spritePath := filepath.Join(previewDir, spriteFileName)
|
||||||
|
|
||||||
|
if mapData, err := readPreviewManifest(manifestPath); err == nil {
|
||||||
|
if mapData.Duration > 0 && fileExists(spritePath) {
|
||||||
|
return mapData, previewKey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock := s.previewLock(previewKey)
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
if mapData, err := readPreviewManifest(manifestPath); err == nil {
|
||||||
|
if mapData.Duration > 0 && fileExists(spritePath) {
|
||||||
|
return mapData, previewKey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(previewDir, 0o755); err != nil {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("create preview cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||||
|
if err != nil {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("ffmpeg not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
ffprobePath, err := exec.LookPath("ffprobe")
|
||||||
|
if err != nil {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("ffprobe not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxWithTimeout, cancel := context.WithTimeout(ctx, previewGenerationLimit)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
duration, err := probePreviewDuration(ctxWithTimeout, ffprobePath, req.Source, req.Referer)
|
||||||
|
if err != nil {
|
||||||
|
if req.Duration > 0 {
|
||||||
|
duration = req.Duration
|
||||||
|
} else {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("probe preview duration: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration <= 0 {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("invalid duration for preview generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := selectPreviewInterval(duration)
|
||||||
|
|
||||||
|
if err := generatePreviewSprite(ctxWithTimeout, ffmpegPath, req.Source, req.Referer, spritePath, interval); err != nil {
|
||||||
|
return PreviewMap{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mapData := buildPreviewMap(duration, interval)
|
||||||
|
if err := writePreviewManifest(manifestPath, mapData); err != nil {
|
||||||
|
return PreviewMap{}, "", fmt.Errorf("write preview manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapData, previewKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PreviewSpritePath(previewKey string) string {
|
||||||
|
trimmed := strings.TrimSpace(previewKey)
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
safeKey := sanitizePreviewKey(trimmed)
|
||||||
|
if safeKey == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(s.previewRoot, safeKey, spriteFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) previewLock(key string) *sync.Mutex {
|
||||||
|
s.previewMu.Lock()
|
||||||
|
defer s.previewMu.Unlock()
|
||||||
|
|
||||||
|
lock, exists := s.previewLocks[key]
|
||||||
|
if exists {
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
newLock := &sync.Mutex{}
|
||||||
|
s.previewLocks[key] = newLock
|
||||||
|
return newLock
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPreviewIdentity(malID int, episode string, mode string, source string, referer string) string {
|
||||||
|
payload := fmt.Sprintf("%d|%s|%s|%s|%s", malID, episode, mode, source, referer)
|
||||||
|
sum := sha1.Sum([]byte(payload))
|
||||||
|
return hex.EncodeToString(sum[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizePreviewKey(raw string) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
for _, char := range raw {
|
||||||
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' {
|
||||||
|
builder.WriteRune(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectPreviewInterval(duration float64) float64 {
|
||||||
|
if duration <= 0 {
|
||||||
|
return previewFrameInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate := duration / float64(previewFrameLimit)
|
||||||
|
if candidate < previewFrameInterval {
|
||||||
|
return previewFrameInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Ceil(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computePreviewFrames(duration float64, interval float64) int {
|
||||||
|
if duration <= 0 || interval <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frames := int(math.Ceil(duration / interval))
|
||||||
|
if frames > previewFrameLimit {
|
||||||
|
return previewFrameLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPreviewMap(duration float64, interval float64) PreviewMap {
|
||||||
|
frames := computePreviewFrames(duration, interval)
|
||||||
|
cues := make([]PreviewCue, 0, frames)
|
||||||
|
|
||||||
|
for idx := 0; idx < frames; idx++ {
|
||||||
|
start := float64(idx) * interval
|
||||||
|
end := start + interval
|
||||||
|
if idx == frames-1 {
|
||||||
|
end = duration
|
||||||
|
}
|
||||||
|
if end > duration {
|
||||||
|
end = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
column := idx % previewGridColumns
|
||||||
|
row := idx / previewGridColumns
|
||||||
|
x := column * previewFrameWidth
|
||||||
|
y := row * previewFrameHeight
|
||||||
|
|
||||||
|
cues = append(cues, PreviewCue{
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
Sprite: spriteFileName,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
Width: previewFrameWidth,
|
||||||
|
Height: previewFrameHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreviewMap{
|
||||||
|
Width: previewFrameWidth,
|
||||||
|
Height: previewFrameHeight,
|
||||||
|
Columns: previewGridColumns,
|
||||||
|
Rows: previewGridRows,
|
||||||
|
Interval: interval,
|
||||||
|
Duration: duration,
|
||||||
|
Cues: cues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPreviewManifest(path string) (PreviewMap, error) {
|
||||||
|
payload, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return PreviewMap{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapData PreviewMap
|
||||||
|
if err := json.Unmarshal(payload, &mapData); err != nil {
|
||||||
|
return PreviewMap{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapData.Duration <= 0 || len(mapData.Cues) == 0 {
|
||||||
|
return PreviewMap{}, fmt.Errorf("invalid preview manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePreviewManifest(path string, mapData PreviewMap) error {
|
||||||
|
payload, err := json.Marshal(mapData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, payload, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func probePreviewDuration(ctx context.Context, ffprobePath string, source string, referer string) (float64, error) {
|
||||||
|
args := []string{
|
||||||
|
"-v", "error",
|
||||||
|
"-show_entries", "format=duration",
|
||||||
|
"-of", "json",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := ffmpegHeaders(referer)
|
||||||
|
if headers != "" {
|
||||||
|
args = append(args, "-headers", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, source)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, ffprobePath, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed ffprobeResult
|
||||||
|
if err := json.Unmarshal(output, &parsed); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
durationText := strings.TrimSpace(parsed.Format.Duration)
|
||||||
|
if durationText == "" {
|
||||||
|
return 0, fmt.Errorf("missing duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := strconv.ParseFloat(durationText, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFinitePositive(duration) {
|
||||||
|
return 0, fmt.Errorf("non-finite duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePreviewSprite(ctx context.Context, ffmpegPath string, source string, referer string, outputPath string, interval float64) error {
|
||||||
|
filter := fmt.Sprintf("fps=1/%0.3f,scale=%d:%d,tile=%dx%d", interval, previewFrameWidth, previewFrameHeight, previewGridColumns, previewGridRows)
|
||||||
|
args := []string{
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-y",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := ffmpegHeaders(referer)
|
||||||
|
if headers != "" {
|
||||||
|
args = append(args, "-headers", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args,
|
||||||
|
"-i", source,
|
||||||
|
"-vf", filter,
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-q:v", "5",
|
||||||
|
"-update", "1",
|
||||||
|
outputPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create ffmpeg stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start ffmpeg: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errPayload, _ := io.ReadAll(io.LimitReader(stderr, 64*1024))
|
||||||
|
waitErr := cmd.Wait()
|
||||||
|
if waitErr != nil {
|
||||||
|
message := strings.TrimSpace(string(errPayload))
|
||||||
|
if message == "" {
|
||||||
|
return fmt.Errorf("ffmpeg failed: %w", waitErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ffmpeg failed: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, statErr := os.Stat(outputPath); statErr != nil {
|
||||||
|
return fmt.Errorf("preview sprite not generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ffmpegHeaders(referer string) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("User-Agent: ")
|
||||||
|
builder.WriteString(defaultUserAgent)
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
|
||||||
|
trimmedReferer := strings.TrimSpace(referer)
|
||||||
|
if trimmedReferer != "" {
|
||||||
|
builder.WriteString("Referer: ")
|
||||||
|
builder.WriteString(trimmedReferer)
|
||||||
|
builder.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("Connection: keep-alive\r\n")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFinitePositive(value float64) bool {
|
||||||
|
if value <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.IsInf(value, 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.IsNaN(value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"mal/internal/jikan"
|
"mal/internal/jikan"
|
||||||
@@ -24,6 +25,9 @@ type Service struct {
|
|||||||
jikanClient *jikan.Client
|
jikanClient *jikan.Client
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
db database.Querier
|
db database.Querier
|
||||||
|
previewRoot string
|
||||||
|
previewMu sync.Mutex
|
||||||
|
previewLocks map[string]*sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type sourceScore struct {
|
type sourceScore struct {
|
||||||
@@ -41,6 +45,8 @@ func NewService(jikanClient *jikan.Client, db database.Querier) *Service {
|
|||||||
jikanClient: jikanClient,
|
jikanClient: jikanClient,
|
||||||
httpClient: &http.Client{Timeout: 12 * time.Second},
|
httpClient: &http.Client{Timeout: 12 * time.Second},
|
||||||
db: db,
|
db: db,
|
||||||
|
previewRoot: newPreviewRootDir(),
|
||||||
|
previewLocks: make(map[string]*sync.Mutex),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,32 @@ type WatchPageData struct {
|
|||||||
Episodes []EpisodeListItem
|
Episodes []EpisodeListItem
|
||||||
Segments []SkipSegment
|
Segments []SkipSegment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreviewRequest struct {
|
||||||
|
MalID int
|
||||||
|
Episode string
|
||||||
|
Mode string
|
||||||
|
Source string
|
||||||
|
Referer string
|
||||||
|
Duration float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewCue struct {
|
||||||
|
Start float64 `json:"start"`
|
||||||
|
End float64 `json:"end"`
|
||||||
|
Sprite string `json:"sprite"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewMap struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Columns int `json:"columns"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
Interval float64 `json:"interval"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
Cues []PreviewCue `json:"cues"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ func NewRouter(cfg Config) http.Handler {
|
|||||||
mux.HandleFunc("/watch/proxy/stream", playbackHandler.HandleProxyStream)
|
mux.HandleFunc("/watch/proxy/stream", playbackHandler.HandleProxyStream)
|
||||||
mux.HandleFunc("/watch/proxy/segment", playbackHandler.HandleProxySegment)
|
mux.HandleFunc("/watch/proxy/segment", playbackHandler.HandleProxySegment)
|
||||||
mux.HandleFunc("/watch/proxy/subtitle", playbackHandler.HandleProxySubtitle)
|
mux.HandleFunc("/watch/proxy/subtitle", playbackHandler.HandleProxySubtitle)
|
||||||
|
mux.HandleFunc("/watch/proxy/preview-map", playbackHandler.HandleProxyPreviewMap)
|
||||||
|
mux.HandleFunc("/watch/proxy/preview-sprite", playbackHandler.HandleProxyPreviewSprite)
|
||||||
|
|
||||||
// Auth Endpoints
|
// Auth Endpoints
|
||||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user