mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-09 06:46:37 +08:00
bug处理
This commit is contained in:
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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":[
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user