bug处理

This commit is contained in:
truewhile
2026-09-09 00:39:00 +08:00
parent ec5f81e6cb
commit 1ea29cca9e
6 changed files with 137 additions and 28 deletions
+35
View File
@@ -1,7 +1,9 @@
package handler package handler
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@@ -359,6 +361,7 @@ func newPlaybackScopeTestRouter(t *testing.T) (*gin.Engine, *service.Container,
Auth: auth, Auth: auth,
Media: service.NewMediaService(cfg, log, repos), Media: service.NewMediaService(cfg, log, repos),
Stream: service.NewStreamService(cfg, log, repos, nil), Stream: service.NewStreamService(cfg, log, repos, nil),
Subtitle: service.NewSubtitleService(cfg, log, repos),
Permissions: permissions, Permissions: permissions,
} }
if err := repos.User.Create(t.Context(), &model.User{ if err := repos.User.Create(t.Context(), &model.User{
@@ -440,6 +443,38 @@ func TestPlaybackInfoForSTRMMediaIncludesHLS(t *testing.T) {
} }
} }
func TestSubtitleListOnlyProbesEmbeddedTracksForHLS(t *testing.T) {
router, svc, secret := newPlaybackScopeTestRouter(t)
loginToken := signedTestToken(t, secret)
resolveCalls := 0
svc.Subtitle.SetStrmPlayTargetResolver(func(context.Context, string) (*service.StrmPlayResult, error) {
resolveCalls++
return nil, errors.New("probe resolver called")
})
request := func(path string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "http://nas.local"+path, nil)
req.Header.Set("Authorization", "Bearer "+loginToken)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w
}
if w := request("/api/media/media-1/subtitles"); w.Code != http.StatusOK {
t.Fatalf("direct subtitle status = %d body=%s", w.Code, w.Body.String())
}
if resolveCalls != 0 {
t.Fatalf("direct subtitle request resolved cloud media %d times, want 0", resolveCalls)
}
if w := request("/api/media/media-1/subtitles?include_embedded=true"); w.Code != http.StatusOK {
t.Fatalf("HLS subtitle status = %d body=%s", w.Code, w.Body.String())
}
if resolveCalls != 1 {
t.Fatalf("HLS subtitle request resolved cloud media %d times, want 1", resolveCalls)
}
}
func TestHLSPlaylistForRemoteEmbyMediaDisabled(t *testing.T) { func TestHLSPlaylistForRemoteEmbyMediaDisabled(t *testing.T) {
router, svc, secret := newPlaybackScopeTestRouter(t) router, svc, secret := newPlaybackScopeTestRouter(t)
svc.EmbyRemote = &service.EmbyRemoteService{} svc.EmbyRemote = &service.EmbyRemoteService{}
+7 -1
View File
@@ -16,7 +16,13 @@ func listSubtitlesHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusOK, gin.H{"tracks": []service.SubtitleTrack{}}) c.JSON(http.StatusOK, gin.H{"tracks": []service.SubtitleTrack{}})
return return
} }
tracks, err := svc.Subtitle.Discover(c.Request.Context(), id) var tracks []service.SubtitleTrack
var err error
if c.Query("include_embedded") == "true" {
tracks, err = svc.Subtitle.Discover(c.Request.Context(), id)
} else {
tracks, err = svc.Subtitle.DiscoverExternalOnly(c.Request.Context(), id)
}
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return return
+35 -19
View File
@@ -94,28 +94,28 @@ func (s *SubtitleService) Discover(ctx context.Context, mediaID string) ([]Subti
// DiscoverExternalOnly 只返回媒体旁边的外挂字幕文件,不含容器内嵌字幕轨。 // DiscoverExternalOnly 只返回媒体旁边的外挂字幕文件,不含容器内嵌字幕轨。
// Emby 字幕接口(/Videos/:id/Subtitles/...)用。 // Emby 字幕接口(/Videos/:id/Subtitles/...)用。
func (s *SubtitleService) DiscoverExternalOnly(ctx context.Context, mediaID string) ([]SubtitleTrack, error) { func (s *SubtitleService) DiscoverExternalOnly(ctx context.Context, mediaID string) ([]SubtitleTrack, error) {
tracks, err := s.discover(ctx, mediaID) cacheKey := "external:" + mediaID
if tracks, ok := s.cachedDiscovery(cacheKey); ok {
return tracks, nil
}
tracks, err := s.discoverExternalUncached(ctx, mediaID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
out := make([]SubtitleTrack, 0, len(tracks)) s.rememberDiscovery(cacheKey, tracks)
for _, track := range tracks { return tracks, nil
if track.Source != "embedded" {
out = append(out, track)
}
}
return out, nil
} }
func (s *SubtitleService) discover(ctx context.Context, mediaID string) ([]SubtitleTrack, error) { func (s *SubtitleService) discover(ctx context.Context, mediaID string) ([]SubtitleTrack, error) {
if tracks, ok := s.cachedDiscovery(mediaID); ok { cacheKey := "all:" + mediaID
if tracks, ok := s.cachedDiscovery(cacheKey); ok {
return tracks, nil return tracks, nil
} }
tracks, err := s.discoverUncached(ctx, mediaID) tracks, err := s.discoverUncached(ctx, mediaID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.rememberDiscovery(mediaID, tracks) s.rememberDiscovery(cacheKey, tracks)
return tracks, nil return tracks, nil
} }
@@ -159,6 +159,30 @@ func (s *SubtitleService) discoverUncached(ctx context.Context, mediaID string)
if m == nil { if m == nil {
return nil, errors.New("media not found") return nil, errors.New("media not found")
} }
tracks := discoverExternalSubtitleTracks(m)
embedded, err := s.discoverEmbedded(ctx, m)
if err != nil {
if s.log != nil {
s.log.Debug("discover embedded subtitles failed", zap.String("media_id", mediaID), zap.Error(err))
}
} else {
tracks = append(tracks, embedded...)
}
return tracks, nil
}
func (s *SubtitleService) discoverExternalUncached(ctx context.Context, mediaID string) ([]SubtitleTrack, error) {
m, err := s.repo.Media.FindByID(ctx, mediaID)
if err != nil {
return nil, err
}
if m == nil {
return nil, errors.New("media not found")
}
return discoverExternalSubtitleTracks(m), nil
}
func discoverExternalSubtitleTracks(m *model.Media) []SubtitleTrack {
dir := filepath.Dir(m.Path) dir := filepath.Dir(m.Path)
bases := mediaSidecarBaseVariants(m.Path) bases := mediaSidecarBaseVariants(m.Path)
if len(bases) == 0 { if len(bases) == 0 {
@@ -214,15 +238,7 @@ func (s *SubtitleService) discoverUncached(ctx context.Context, mediaID string)
}) })
} }
} }
embedded, err := s.discoverEmbedded(ctx, m) return tracks
if err != nil {
if s.log != nil {
s.log.Debug("discover embedded subtitles failed", zap.String("media_id", mediaID), zap.Error(err))
}
} else {
tracks = append(tracks, embedded...)
}
return tracks, nil
} }
type embeddedSubtitleProbe struct { type embeddedSubtitleProbe struct {
+39
View File
@@ -2,7 +2,9 @@ package service
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -48,6 +50,43 @@ func TestSubtitleDiscoverNoTracksReturnsEmptySlice(t *testing.T) {
} }
} }
func TestDiscoverExternalOnlyDoesNotResolveOrProbeCloudMedia(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil {
t.Fatal(err)
}
media := model.Media{
Title: "Cloud Media",
Path: "cloud://115/example/video.mkv",
STRMURL: "cloud://115/example/video.mkv",
}
if err := db.Create(&media).Error; err != nil {
t.Fatal(err)
}
svc := NewSubtitleService(&config.Config{}, zap.NewNop(), repository.New(db))
resolveCalls := 0
svc.SetStrmPlayTargetResolver(func(context.Context, string) (*StrmPlayResult, error) {
resolveCalls++
return nil, errors.New("resolver must not be called")
})
tracks, err := svc.DiscoverExternalOnly(t.Context(), media.ID)
if err != nil {
t.Fatal(err)
}
if resolveCalls != 0 {
t.Fatalf("STRM resolver called %d times, want 0", resolveCalls)
}
if len(tracks) != 0 {
t.Fatalf("len(tracks) = %d, want 0", len(tracks))
}
}
func TestEmbeddedSubtitleProbeClassifiesTextAndBitmapTracks(t *testing.T) { func TestEmbeddedSubtitleProbeClassifiesTextAndBitmapTracks(t *testing.T) {
var probe embeddedSubtitleProbe var probe embeddedSubtitleProbe
raw := []byte(`{"streams":[ raw := []byte(`{"streams":[
+4 -2
View File
@@ -13,9 +13,11 @@ export interface SubtitleTrack {
} }
export const subtitlesAPI = { export const subtitlesAPI = {
list: (mediaId: string) => list: (mediaId: string, includeEmbedded = false) =>
api api
.get<{ tracks: SubtitleTrack[] | null }>(`/media/${mediaId}/subtitles`) .get<{ tracks: SubtitleTrack[] | null }>(`/media/${mediaId}/subtitles`, {
params: includeEmbedded ? { include_embedded: 'true' } : undefined,
})
.then((r) => r.data.tracks ?? []), .then((r) => r.data.tracks ?? []),
url: (mediaId: string, path: string) => { url: (mediaId: string, path: string) => {
+17 -6
View File
@@ -237,8 +237,22 @@ export function PlayerPage() {
// 404 / 无权限等:给出可见错误提示,避免永久「加载中」 // 404 / 无权限等:给出可见错误提示,避免永久「加载中」
setLoadError(`无法加载该媒体:${apiErrorMessage(err)}`) setLoadError(`无法加载该媒体:${apiErrorMessage(err)}`)
}) })
return () => {
cancelled = true
}
}, [id, modeParam, directOnly])
// Wire up the actual <video> element when we know the mode.
// Depend on media.id (not the media object): refreshing duration after
// MANIFEST_PARSED must not remount HLS or it storms EnsureJob / DELETE.
const mediaId = media?.id
// 直连播放只发现外挂字幕;只有 HLS 模式需要探测可烧录的内嵌字幕。
useEffect(() => {
if (!mediaId) return
let cancelled = false
subtitlesAPI subtitlesAPI
.list(id) .list(mediaId, mode === 'hls')
.then((tracks) => { .then((tracks) => {
if (cancelled) return if (cancelled) return
const list = tracks ?? [] const list = tracks ?? []
@@ -249,16 +263,13 @@ export function PlayerPage() {
.catch(() => { .catch(() => {
if (cancelled) return if (cancelled) return
setSubs([]) setSubs([])
setSubtitleIndex(-1)
}) })
return () => { return () => {
cancelled = true cancelled = true
} }
}, [id, modeParam, directOnly]) }, [mediaId, mode])
// Wire up the actual <video> element when we know the mode.
// Depend on media.id (not the media object): refreshing duration after
// MANIFEST_PARSED must not remount HLS or it storms EnsureJob / DELETE.
const mediaId = media?.id
const selectedSubtitle = subtitleIndex >= 0 ? subs[subtitleIndex] : undefined const selectedSubtitle = subtitleIndex >= 0 ? subs[subtitleIndex] : undefined
const burnedSubtitleStream = const burnedSubtitleStream =
selectedSubtitle?.delivery === 'burn' ? selectedSubtitle.stream_index : undefined selectedSubtitle?.delivery === 'burn' ? selectedSubtitle.stream_index : undefined