Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
package m3u8ticket
import (
"reflect"
"91porn-server/common"
"91porn-server/models/v/vidmod"
"github.com/gin-gonic/gin"
)
// maxSignDepth 反射兜底遍历响应体的最大深度,防止异常对象图导致过深递归(视频响应本身是浅树)。
const maxSignDepth = 8
// ticketSigner 实现 vidmod.M3u8Signer:持有本次请求的签票上下文(用户、客户端 IP/UA),
// 由各响应体的 SignM3u8 回调,对交出的每个 m3u8 地址字段就地签票。
type ticketSigner struct {
userID uint64
ip string
ua string
}
// SignM3u8URL 对单个 m3u8 地址字段就地签票;空串或签发失败(绝对地址、非 m3u8 等)时保持原值不变。
func (s *ticketSigner) SignM3u8URL(field *string, playable, preview bool) {
if field == nil || *field == "" {
return
}
if signed := BuildSignedURL(s.userID, *field, s.ip, s.ua, playable, preview); signed != "" {
*field = signed
}
}
func requestIP(c *gin.Context) string {
if c == nil {
return ""
}
return common.GetIP(c)
}
func requestUA(c *gin.Context) string {
if c == nil {
return ""
}
u, err := common.GetUA(c)
if err != nil {
return ""
}
return u.UserAgent
}
// Sign 是 SignResponse 的便捷封装:自动从上下文取登录用户ID,控制器只需在返回前加一行
// m3u8ticket.Sign(ctx, data) 即可对响应体里的视频 m3u8 地址按需签票。
func Sign(c *gin.Context, data interface{}) {
if c == nil {
return
}
SignResponse(c, common.TryGetUID(c), data)
}
// SignResponse 是各视频接口的统一入口:对响应体 data 里的视频 m3u8 地址就地签票(不再限 H5)。
//
// data 只要实现 vidmod.M3u8Signable(所有携带视频地址的响应体都已实现)即可零反射签票;另外兼容控制器直接
// 下发的裸切片(recommser.GetAd 等)与 gin.H(RecommendList 等)。少数尚未实现接口的历史/边缘类型走反射兜底,
// 保证不漏签。data 为 nil / userID 为 0 / 未配置密钥(BuildSignedURL 返回空)时零副作用,兼容灰度未配置密钥。
func SignResponse(c *gin.Context, userID uint64, data interface{}) {
if data == nil || userID == 0 {
return
}
// data 可能是服务出错时返回的类型化 nil 指针((*Resp)(nil)):接口本身非 nil,但对值接收者方法会解引用
// nil 而 panic,先挡掉(等价于旧反射对 nil 指针的 IsNil 保护)。
if rv := reflect.ValueOf(data); rv.Kind() == reflect.Ptr && rv.IsNil() {
return
}
signData(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}, data)
}
// signData 把 data 分派给对应签票实现:优先零反射的接口/裸切片/map,其余历史类型走反射兜底。
func signData(s vidmod.M3u8Signer, data interface{}) {
switch d := data.(type) {
case nil:
return
case vidmod.M3u8Signable:
d.SignM3u8(s)
case []*vidmod.VideoInfo:
vidmod.SignM3u8Infos(s, d)
case []*vidmod.VideoInfoResp:
vidmod.SignM3u8Resps(s, d)
case gin.H:
for _, v := range d {
signData(s, v)
}
default:
signByReflect(s, reflect.ValueOf(data), 0)
}
}
// signByReflect 是反射兜底:仅用于尚未实现 vidmod.M3u8Signable 的历史/边缘响应类型,语义与旧实现一致——
// 遍历对象图,命中 *VideoInfo/*VideoInfoResp 即签票。正常高频接口都走上面的接口分支,不会到这里。
// 待剩余类型补齐 SignM3u8 后,可整体删除本函数及 reflect 依赖。
func signByReflect(s vidmod.M3u8Signer, rv reflect.Value, depth int) {
if depth > maxSignDepth || !rv.IsValid() {
return
}
switch rv.Kind() {
case reflect.Ptr:
if rv.IsNil() {
return
}
switch v := rv.Interface().(type) {
case *vidmod.VideoInfo:
v.SignM3u8(s)
return
case *vidmod.VideoInfoResp:
v.SignM3u8(s)
return
}
signByReflect(s, rv.Elem(), depth+1)
case reflect.Interface:
if rv.IsNil() {
return
}
signByReflect(s, rv.Elem(), depth+1)
case reflect.Struct:
if rv.CanAddr() {
switch v := rv.Addr().Interface().(type) {
case *vidmod.VideoInfo:
v.SignM3u8(s)
return
case *vidmod.VideoInfoResp:
v.SignM3u8(s)
return
}
}
t := rv.Type()
for i := 0; i < rv.NumField(); i++ {
if t.Field(i).PkgPath != "" { // 跳过未导出字段,reflect 无法安全访问
continue
}
signByReflect(s, rv.Field(i), depth+1)
}
case reflect.Slice, reflect.Array:
for i := 0; i < rv.Len(); i++ {
signByReflect(s, rv.Index(i), depth+1)
}
case reflect.Map:
for _, k := range rv.MapKeys() {
signByReflect(s, rv.MapIndex(k), depth+1)
}
}
}
// SignURL 供只有单个 m3u8 地址字段的响应(官网视频、媒体内容等)复用。
// 不再限制 H5:已配置密钥时改写,未配置(BuildSignedURL 返回空)则原样返回,兼容灰度未配置密钥的情况。
func SignURL(c *gin.Context, userID uint64, field *string, playable, preview bool) {
if field == nil || *field == "" || userID == 0 {
return
}
(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}).SignM3u8URL(field, playable, preview)
}