Files
djk/internal/app/ui_emby_cover.go
T

118 lines
3.7 KiB
Go

package app
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"net/url"
"strings"
"time"
)
func embyMetadataID(key []byte, dramaID string) string {
mac := hmac.New(sha256.New, key)
mac.Write([]byte("emby-metadata-v1\x00" + dramaID))
return hex.EncodeToString(mac.Sum(nil))
}
func embyCoverToken(key []byte, dramaID, owner, version string) string {
mac := hmac.New(sha256.New, key)
mac.Write([]byte("emby-cover-v1\x00" + owner + "\x00" + dramaID + "\x00" + version))
return hex.EncodeToString(mac.Sum(nil))
}
func embyDramaCover(drama Drama) string {
raw := bestDramaCover(drama)
if raw == "" || len(raw) > 4096 {
return ""
}
remote, ok := buildImageURL(raw)
if !ok {
return ""
}
return remote
}
func embyCoverURL(drama Drama, base string, key []byte, owner string) string {
remote := embyDramaCover(drama)
if remote == "" {
return ""
}
digest := sha256.Sum256([]byte(remote))
version := hex.EncodeToString(digest[:16])
query := url.Values{"id": {drama.ID}, "v": {version}, "key": {embyCoverToken(key, drama.ID, owner, version)}}
if owner != "" {
query.Set("account", owner)
}
return base + "/api/emby/cover?" + query.Encode()
}
func (app *UIApp) embyCoverSource(id string) string {
app.mu.Lock()
for _, drama := range app.dramas {
if drama.ID == id {
remote := embyDramaCover(drama)
if remote != "" {
app.mu.Unlock()
return remote
}
break
}
}
app.mu.Unlock()
manager := app.embySyncer()
manager.mu.Lock()
remote := manager.document.Entries[id].Poster.SourceURL
manager.mu.Unlock()
if remote != "" {
if normalized, ok := buildImageURL(remote); ok {
return normalized
}
}
return ""
}
func (app *UIApp) handleEmbyCover(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Cache-Control", "private, no-store")
writer.Header().Set("X-Content-Type-Options", "nosniff")
if request.Method != http.MethodGet && request.Method != http.MethodHead {
writer.Header().Set("Allow", "GET, HEAD")
writeJSON(writer, http.StatusMethodNotAllowed, map[string]string{"error": "请求方法不支持"})
return
}
query := request.URL.Query()
id, owner, version := query.Get("id"), query.Get("account"), query.Get("v")
canonical, _, valid := playbackHistoryIdentity(id)
supplied, err := hex.DecodeString(query.Get("key"))
versionBytes, versionErr := hex.DecodeString(version)
key, keyErr := app.embySigningKey(false)
if !valid || id != canonical || len(id) > 256 || len(owner) > 256 || strings.ContainsAny(owner, "\x00\r\n") ||
err != nil || len(supplied) != 32 || versionErr != nil || len(versionBytes) != 16 || keyErr != nil {
writeJSON(writer, http.StatusForbidden, map[string]string{"error": "Emby 海报链接无效,请重新同步或导出"})
return
}
expected, _ := hex.DecodeString(embyCoverToken(key, id, owner, version))
if !hmac.Equal(expected, supplied) || owner != "" && !app.embyAccountSourceAllowed(owner, id) {
writeJSON(writer, http.StatusForbidden, map[string]string{"error": "Emby 海报链接无效或账号权限已变更"})
return
}
remote := app.embyCoverSource(id)
if remote == "" {
writeJSON(writer, http.StatusNotFound, map[string]string{"error": "此剧暂无可用海报"})
return
}
source, _, _ := splitProviderDramaID(id)
ctx, cancel := context.WithTimeout(context.WithValue(request.Context(), coverSourceKey{}, source), 90*time.Second)
defer cancel()
data, err := app.loadCoverImage(ctx, remote, decodeImageBytes)
if err != nil {
writeJSON(writer, http.StatusBadGateway, map[string]string{"error": "读取海报失败:" + app.redactError(err)})
return
}
writer.Header().Set("Content-Type", imageContentType(data))
http.ServeContent(writer, request, "cover", time.Time{}, bytes.NewReader(data))
}