mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-12 19:34:24 +08:00
101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
// libraryCoverHandler 输出媒体库封面图(Emby 风格的成员海报拼图)。
|
|
//
|
|
// 之前 Web 端是拿两张成员海报在前端用 CSS 拼格子,Emby 客户端则直接拿一张
|
|
// 评分最高的海报,结果同一个库在不同客户端长得完全不一样。现在服务端生成
|
|
// 一张 16:9 的拼图(Emby 的 folder image 做法),Emby 客户端、Web 和 App
|
|
// 都指向这一份,外观统一。
|
|
func libraryCoverHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id := strings.TrimSpace(c.Param("id"))
|
|
if id == "" || svc == nil || svc.ImageProxy == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "cover unavailable"})
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 25*time.Second)
|
|
defer cancel()
|
|
|
|
sources, lib := service.LibraryCoverSourcesForLibrary(ctx, svc.Repo, id)
|
|
if lib == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "library not found"})
|
|
return
|
|
}
|
|
if strings.TrimSpace(lib.CoverURL) != "" {
|
|
// 管理员自定义封面优先,保持既有行为(代理该地址)。
|
|
if err := svc.ImageProxy.Serve(ctx, c.Writer, c.Request, lib.CoverURL); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "custom cover unavailable"})
|
|
}
|
|
return
|
|
}
|
|
if len(sources) == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "no library items with artwork"})
|
|
return
|
|
}
|
|
width, height := libraryCoverRequestSize(c)
|
|
data, err := svc.ImageProxy.LibraryCoverImage(ctx, id, sources, width, height)
|
|
if err != nil || len(data) == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "cover generation failed"})
|
|
return
|
|
}
|
|
writeLibraryCoverImage(c, data, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// libraryCoverRequestSize 解析请求的封面尺寸:Web 用 w/h,Emby 用
|
|
// maxWidth/maxHeight,两种都兼容。服务端统一输出 16:9 画布(与该库在 Emby
|
|
// 里上报的 PrimaryImageAspectRatio 一致),客户端传的尺寸只当作「不超过」的
|
|
// 边界使用。
|
|
func libraryCoverRequestSize(c *gin.Context) (int, int) {
|
|
return firstPositiveQueryIntParam(c, "maxWidth", "maxwidth", "width", "w"),
|
|
firstPositiveQueryIntParam(c, "maxHeight", "maxheight", "height", "h")
|
|
}
|
|
|
|
func firstPositiveQueryIntParam(c *gin.Context, names ...string) int {
|
|
for _, name := range names {
|
|
raw := strings.TrimSpace(c.Query(name))
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
|
return n
|
|
}
|
|
if f, err := strconv.ParseFloat(raw, 64); err == nil && f > 0 {
|
|
return int(f)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func writeLibraryCoverImage(c *gin.Context, data []byte, status int) {
|
|
sum := sha256.Sum256(data)
|
|
etag := `"` + hex.EncodeToString(sum[:8]) + `"`
|
|
c.Header("ETag", etag)
|
|
c.Header("Cache-Control", "public, max-age=300, stale-while-revalidate=86400")
|
|
if match := strings.TrimSpace(c.GetHeader("If-None-Match")); match != "" && match == etag {
|
|
c.Status(http.StatusNotModified)
|
|
return
|
|
}
|
|
c.Header("Content-Type", "image/jpeg")
|
|
c.Header("Content-Length", strconv.Itoa(len(data)))
|
|
if c.Request.Method == http.MethodHead {
|
|
c.Status(status)
|
|
return
|
|
}
|
|
c.Data(status, "image/jpeg", data)
|
|
}
|