fix: reduce emby load and stabilize strm scan

This commit is contained in:
ShukeBta
2026-06-11 00:18:51 +08:00
parent 859d6e9d24
commit 542e85a067
22 changed files with 1040 additions and 91 deletions
+3
View File
@@ -269,6 +269,9 @@ func updateSettingHandler(svc *service.Container) gin.HandlerFunc {
if req.Key == "transcode.hw_enabled" || req.Key == "transcode.hw_accel" || req.Key == "transcoder.hardware_accel" || req.Key == "transcoder.encoder" {
svc.Transcoder.StopAll()
}
if req.Key == "cloud.auto_sync_enabled" && !service.ParseBoolSetting(req.Value, false) && svc.Scan != nil {
_ = svc.Scan.CancelAllCloudScans()
}
c.Status(http.StatusNoContent)
}
}
+26 -2
View File
@@ -7,6 +7,7 @@ package handler
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
@@ -596,6 +597,17 @@ func embyResumeItemsHandler(svc *service.Container) gin.HandlerFunc {
}
}
func embyItemsCountsHandler(_ *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"MovieCount": 0,
"SeriesCount": 0,
"EpisodeCount": 0,
"ItemCount": 0,
})
}
}
// ─── Images ──────────────────────────────────────────────────────────────────
// embyItemImageHandler 把 /Items/{id}/Images/Primary 等请求直接输出为图片。
@@ -603,14 +615,18 @@ func embyResumeItemsHandler(svc *service.Container) gin.HandlerFunc {
// /api/img 会变成 401,所以这里复用 ImageProxy 但不再走 /api 路由。
func embyItemImageHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 8*time.Second)
defer cancel()
req := c.Request.WithContext(ctx)
id := c.Param("id")
imgType := strings.ToLower(c.Param("type"))
raw, err := svc.Emby.ImageURL(c.Request.Context(), id, imgType)
raw, err := svc.Emby.ImageURL(ctx, id, imgType)
if err != nil || raw == "" {
c.Status(http.StatusNotFound)
return
}
if typ, ref, ok := parseCloudPlayImageURL(raw); ok {
c.Request = req
serveCloudResolvedLink(svc, c, typ, ref)
return
}
@@ -618,7 +634,7 @@ func embyItemImageHandler(svc *service.Container) gin.HandlerFunc {
c.Status(http.StatusNotFound)
return
}
if err := svc.ImageProxy.Serve(c.Request.Context(), c.Writer, c.Request, raw); err != nil {
if err := svc.ImageProxy.Serve(ctx, c.Writer, req, raw); err != nil {
c.Status(http.StatusNotFound)
}
}
@@ -1059,6 +1075,8 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
auth.GET("/Items", embyItemsHandler(svc))
auth.GET("/Users/:userId/Items", embyItemsHandler(svc))
auth.GET("/Items/Counts", embyItemsCountsHandler(svc))
auth.GET("/Users/:userId/Items/Counts", embyItemsCountsHandler(svc))
auth.GET("/Items/:id", embyItemByIDHandler(svc))
auth.GET("/Users/:userId/Items/:id", embyUserItemByIDHandler(svc))
auth.GET("/Shows/:id/Seasons", embyShowSeasonsHandler(svc))
@@ -1079,7 +1097,9 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
auth.GET("/Videos/:id/stream.:container", embyVideoStreamHandler(svc))
auth.HEAD("/Videos/:id/stream.:container", embyVideoStreamHandler(svc))
auth.GET("/Videos/:id/original", embyVideoStreamHandler(svc))
auth.HEAD("/Videos/:id/original", embyVideoStreamHandler(svc))
auth.GET("/Videos/:id/original.:container", embyVideoStreamHandler(svc))
auth.HEAD("/Videos/:id/original.:container", embyVideoStreamHandler(svc))
auth.GET("/Videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
auth.HEAD("/Videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
auth.GET("/Videos/:id/main.m3u8", embyVideoHLSPlaylistHandler(svc))
@@ -1121,6 +1141,8 @@ func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Contain
auth.GET("/items", embyItemsHandler(svc))
auth.GET("/users/:userId/items", embyItemsHandler(svc))
auth.GET("/items/counts", embyItemsCountsHandler(svc))
auth.GET("/users/:userId/items/counts", embyItemsCountsHandler(svc))
auth.GET("/items/:id", embyItemByIDHandler(svc))
auth.GET("/users/:userId/items/:id", embyUserItemByIDHandler(svc))
auth.GET("/shows/:id/seasons", embyShowSeasonsHandler(svc))
@@ -1141,7 +1163,9 @@ func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Contain
auth.GET("/videos/:id/stream.:container", embyVideoStreamHandler(svc))
auth.HEAD("/videos/:id/stream.:container", embyVideoStreamHandler(svc))
auth.GET("/videos/:id/original", embyVideoStreamHandler(svc))
auth.HEAD("/videos/:id/original", embyVideoStreamHandler(svc))
auth.GET("/videos/:id/original.:container", embyVideoStreamHandler(svc))
auth.HEAD("/videos/:id/original.:container", embyVideoStreamHandler(svc))
auth.GET("/videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
auth.HEAD("/videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
auth.GET("/videos/:id/main.m3u8", embyVideoHLSPlaylistHandler(svc))
+103
View File
@@ -276,6 +276,50 @@ func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
}
}
func TestEmbyItemsCountsRouteReturnsJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(&model.User{}); err != nil {
t.Fatalf("migrate: %v", err)
}
repos := repository.New(db)
if err := repos.User.Create(t.Context(), &model.User{
Base: model.Base{ID: "user-1"},
Username: "tester",
PasswordHash: "x",
Role: "admin",
Tier: "plus",
IsActive: true,
}); err != nil {
t.Fatalf("create user: %v", err)
}
const secret = "test-secret"
router := gin.New()
registerEmbyRoutes(router, secret, &service.Container{Repo: repos})
for _, path := range []string{"/Items/Counts", "/Users/user-1/Items/Counts", "/items/counts"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("X-Emby-Token", signedTestToken(t, secret))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("%s status=%d body=%s", path, w.Code, w.Body.String())
}
var body map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("%s decode response: %v", path, err)
}
if _, ok := body["MovieCount"]; !ok {
t.Fatalf("%s missing MovieCount: %#v", path, body)
}
}
}
func TestEmbyItemImageServesWithoutAPIAuth(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
@@ -531,6 +575,65 @@ func TestEmbyLowercaseVideoStreamRouteServesMedia(t *testing.T) {
}
}
func TestEmbyLowercaseOriginalHeadRouteServesHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := db.AutoMigrate(model.AllModels()...); err != nil {
t.Fatalf("migrate: %v", err)
}
repos := repository.New(db)
if err := repos.User.Create(t.Context(), &model.User{
Base: model.Base{ID: "user-1"},
Username: "tester",
PasswordHash: "x",
Role: "admin",
Tier: "plus",
IsActive: true,
}); err != nil {
t.Fatalf("create user: %v", err)
}
dir := t.TempDir()
mediaPath := filepath.Join(dir, "sample.mp4")
if err := os.WriteFile(mediaPath, []byte("fake-video-bytes"), 0o644); err != nil {
t.Fatalf("write media: %v", err)
}
lib := model.Library{Name: "电影", Path: dir, Type: "movie", Enabled: true}
if err := repos.Library.Create(t.Context(), &lib); err != nil {
t.Fatalf("create library: %v", err)
}
if err := db.Create(&model.Media{
Base: model.Base{ID: "media-1"},
LibraryID: lib.ID,
Title: "Lowercase Original",
Path: mediaPath,
Container: "mp4",
}).Error; err != nil {
t.Fatalf("create media: %v", err)
}
const secret = "test-secret"
router := gin.New()
registerEmbyRoutes(router, secret, &service.Container{
Repo: repos,
Emby: service.NewEmbyService(&config.Config{}, zap.NewNop(), repos),
Stream: service.NewStreamService(&config.Config{}, zap.NewNop(), repos, nil),
})
req := httptest.NewRequest(http.MethodHead, "/videos/media-1/original.mp4?api_key="+signedTestToken(t, secret), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("unexpected status: %d body=%s", w.Code, w.Body.String())
}
if w.Body.Len() != 0 {
t.Fatalf("HEAD response should not include body, got %q", w.Body.String())
}
}
func TestEmbyLowercaseVideoHLSRouteDoesNot404WhenDirectOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})