mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
76 lines
2.9 KiB
Go
76 lines
2.9 KiB
Go
// Package handler — 片头/片尾片段接口。
|
|
//
|
|
// GET /playback/:id/segments
|
|
//
|
|
// 单独开一个接口而不是塞进 /media/:id/playback,有两个原因:一是抓取外部数据
|
|
// 可能要几秒,不能拖慢决定能否起播的那个请求;二是片段与播放来源(本地 / 云盘 /
|
|
// 远程 Emby)无关,独立出来对所有媒体一致。
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
// playbackSegmentsHandler returns the skippable ranges for one media item.
|
|
// The client calls it after playback has already started, so the provider
|
|
// lookup never delays a play.
|
|
func playbackSegmentsHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
autoSkip := resolveAutoSkipFlag(c, svc)
|
|
source := resolveSegmentSource(c, svc)
|
|
segments := []service.SegmentView{}
|
|
pending := false
|
|
|
|
m, err := findMediaForPlaybackEndpoint(c, svc, c.Param("id"))
|
|
if err != nil || m == nil || !mediaVisibleForRequest(c, svc, m) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "media not found"})
|
|
return
|
|
}
|
|
// 远程 Emby 挂载的条目是上游库的投影,本地没有可查询的外部 ID 关联。
|
|
if svc.Segments != nil && !service.IsEmbyRemoteID(m.ID) {
|
|
result, listErr := svc.Segments.SegmentsForPlayback(c.Request.Context(), m, source)
|
|
if listErr != nil && svc.Log != nil {
|
|
svc.Log.Debug("list media segments failed",
|
|
zap.String("media_id", m.ID), zap.Error(listErr))
|
|
}
|
|
segments = service.ToSegmentViews(result.Segments)
|
|
pending = result.Pending
|
|
}
|
|
// pending 告诉客户端「章节提取还在后台跑,过几秒再拉一次」;提取完成时
|
|
// 如果片头还没播完,跳过按钮就会自己出现,已经过了片头则不会提示。
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"segments": segments,
|
|
"auto_skip": autoSkip,
|
|
"pending": pending,
|
|
"source": source,
|
|
})
|
|
}
|
|
}
|
|
|
|
// resolveAutoSkipFlag reads the「自动跳过片头」switch off whichever profile is
|
|
// currently in effect. It reuses selectedPlayProfile so the server agrees with
|
|
// the UI about which profile is active (explicit header first, then the user's
|
|
// default profile), and a PIN-locked profile never silently skips for the user.
|
|
func resolveAutoSkipFlag(c *gin.Context, svc *service.Container) bool {
|
|
profile, locked := selectedPlayProfile(c, svc)
|
|
if locked || profile == nil {
|
|
return false
|
|
}
|
|
return profile.SkipIntro
|
|
}
|
|
|
|
// resolveSegmentSource reads the「片头片尾数据来源」choice off the active profile.
|
|
// PIN-locked profiles fall back to auto rather than leaking the profile's setting.
|
|
func resolveSegmentSource(c *gin.Context, svc *service.Container) string {
|
|
profile, locked := selectedPlayProfile(c, svc)
|
|
if locked || profile == nil {
|
|
return service.SegmentSourceAuto
|
|
}
|
|
return service.NormalizeSegmentSource(profile.SegmentSource)
|
|
}
|