diff --git a/internal/model/movie.go b/internal/model/movie.go index 232d49c..65125a5 100644 --- a/internal/model/movie.go +++ b/internal/model/movie.go @@ -25,18 +25,25 @@ func (m *Movie) BeforeCreate(tx *gorm.DB) error { } type BaseMovie struct { - Url string `json:"url"` + Url string `json:"url,omitempty"` Name string `gorm:"not null" json:"name"` - Live bool `json:"live"` - Proxy bool `json:"proxy"` - RtmpSource bool `json:"rtmpSource"` - Type string `json:"type"` - Headers map[string]string `gorm:"serializer:fastjson" json:"headers"` - VendorInfo `gorm:"embedded;embeddedPrefix:vendor_info_" json:"vendorInfo"` + Live bool `json:"live,omitempty"` + Proxy bool `json:"proxy,omitempty"` + RtmpSource bool `json:"rtmpSource,omitempty"` + Type string `json:"type,omitempty"` + Headers map[string]string `gorm:"serializer:fastjson" json:"headers,omitempty"` + VendorInfo `gorm:"embedded;embeddedPrefix:vendor_info_" json:"vendorInfo,omitempty"` } type VendorInfo struct { - Vendor StreamingVendor `json:"vendor"` - Shared bool `gorm:"not null;default:false" json:"shared"` - Info map[string]any `gorm:"serializer:fastjson" json:"info"` + Vendor StreamingVendor `json:"vendor"` + Shared bool `gorm:"not null;default:false" json:"shared"` + BilibiliVendorInfo BilibiliVendorInfo `gorm:"embedded;embeddedPrefix:bilibili_" json:"bilibiliVendorInfo,omitempty"` +} + +type BilibiliVendorInfo struct { + Bvid string `json:"bvid,omitempty"` + Cid uint `json:"cid,omitempty"` + Epid uint `json:"epid,omitempty"` + Quality uint `json:"quality,omitempty"` } diff --git a/internal/op/movie.go b/internal/op/movie.go index e9fbf7f..8d2e814 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -147,71 +147,40 @@ func (m *movie) init() (err error) { return nil } -func (m *movie) validateVendorMovie() (err error) { +func (m *movie) validateVendorMovie() error { if m.Base.VendorInfo.Vendor == "" { - return + return nil } switch m.Base.VendorInfo.Vendor { case model.StreamingVendorBilibili: - info := map[string]any{} - bvidI := m.Base.VendorInfo.Info["bvid"] - epIdI := m.Base.VendorInfo.Info["epId"] - if bvidI != nil && epIdI != nil { - return fmt.Errorf("bvid(%v) and epId(%v) can't be used at the same time", bvidI, epIdI) + info := m.Base.VendorInfo.BilibiliVendorInfo + if info.Bvid == "" && info.Epid == 0 { + return fmt.Errorf("bvid and epid are empty") } - if bvidI != nil { - bvid, ok := bvidI.(string) - if !ok { - return fmt.Errorf("bvid is not string") - } - info["bvid"] = bvid - } else if epIdI != nil { - epId, ok := epIdI.(float64) - if !ok { - return fmt.Errorf("epId is not number") - } - info["epId"] = epId - } else { - return fmt.Errorf("bvid and epId is empty") + if info.Bvid != "" && info.Epid != 0 { + return fmt.Errorf("bvid and epid can't be set at the same time") } - qnI, ok := m.Base.VendorInfo.Info["qn"] - if ok { - qn, ok := qnI.(float64) - if !ok { - return fmt.Errorf("qn is not number") - } - info["qn"] = qn + if info.Bvid != "" && info.Cid == 0 { + return fmt.Errorf("cid is empty") } - if bvidI != nil { - cidI := m.Base.VendorInfo.Info["cid"] - if cidI == nil { - return fmt.Errorf("cid is empty") - } - cid, ok := cidI.(float64) - if !ok { - return fmt.Errorf("cid is not number") + if m.Base.Headers == nil { + m.Base.Headers = map[string]string{ + "Referer": "https://www.bilibili.com", + "User-Agent": utils.UA, } - info["cid"] = cid - - m.Base.Url = "" - m.Base.Proxy = false - m.Base.RtmpSource = false - m.Base.VendorInfo.Info = info - return nil } else { - m.Base.Url = "" - m.Base.RtmpSource = false - m.Base.Proxy = true - m.Base.VendorInfo.Info = info - return nil + m.Base.Headers["Referer"] = "https://www.bilibili.com" + m.Base.Headers["User-Agent"] = utils.UA } default: return fmt.Errorf("vendor not support") } + + return nil } func (m *movie) Terminate() { diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 1dbecff..37de922 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -5,13 +5,11 @@ import ( "fmt" "net/http" "path" - "path/filepath" "strconv" "strings" "time" "github.com/gin-gonic/gin" - "github.com/go-resty/resty/v2" "github.com/synctv-org/synctv/internal/conf" "github.com/synctv-org/synctv/internal/db" dbModel "github.com/synctv-org/synctv/internal/model" @@ -68,7 +66,7 @@ func MovieList(ctx *gin.Context) { } } - current, err := genCurrent(room.Current(), user) + current, err := genCurrent(room.Current(), user.ID) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return @@ -83,9 +81,9 @@ func MovieList(ctx *gin.Context) { })) } -func genCurrent(current *op.Current, user *op.User) (*op.Current, error) { +func genCurrent(current *op.Current, userID string) (*op.Current, error) { if current.Movie.Base.Vendor != "" { - return current, parse2VendorMovie(user, ¤t.Movie) + return current, parse2VendorMovie(userID, ¤t.Movie) } return current, nil } @@ -105,7 +103,7 @@ func CurrentMovie(ctx *gin.Context) { room := ctx.MustGet("room").(*op.Room) user := ctx.MustGet("user").(*op.User) - current, err := genCurrent(room.Current(), user) + current, err := genCurrent(room.Current(), user.ID) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return @@ -343,7 +341,7 @@ func ChangeCurrentMovie(ctx *gin.Context) { return } - current, err := genCurrent(room.Current(), user) + current, err := genCurrent(room.Current(), user.ID) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return @@ -380,7 +378,7 @@ func ChangeCurrentMovie(ctx *gin.Context) { if err := room.Broadcast(&op.ElementMessage{ ElementMessage: m, BeforeSendFunc: func(sendTo *op.User) error { - current, err := genCurrent(room.Current(), sendTo) + current, err := genCurrent(room.Current(), sendTo.ID) if err != nil { return err } @@ -397,12 +395,6 @@ func ChangeCurrentMovie(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } -var allowedProxyMovieContentType = map[string]struct{}{ - "video/avi": {}, - "video/mp4": {}, - "video/webm": {}, -} - func ProxyMovie(ctx *gin.Context) { roomId := ctx.Param("roomId") if roomId == "" { @@ -428,8 +420,11 @@ func ProxyMovie(ctx *gin.Context) { } if m.Base.VendorInfo.Vendor != "" { - ProxyVendorMovie(ctx, m.Movie) - return + err = parse2VendorMovie(m.Movie.CreatorID, m.Movie) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } } if l, err := utils.ParseURLIsLocalIP(m.Base.Url); err != nil || l { @@ -437,46 +432,11 @@ func ProxyMovie(ctx *gin.Context) { return } - r := resty.New().R() - - for k, v := range m.Base.Headers { - r.SetHeader(k, v) - } - resp, err := r.Head(m.Base.Url) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - defer resp.RawBody().Close() - - if _, ok := allowedProxyMovieContentType[resp.Header().Get("Content-Type")]; !ok { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(fmt.Errorf("this movie type support proxy: %s", resp.Header().Get("Content-Type")))) - return - } - ctx.Status(resp.StatusCode()) - ctx.Header("Content-Type", resp.Header().Get("Content-Type")) - l := resp.Header().Get("Content-Length") - ctx.Header("Content-Length", l) - ctx.Header("Content-Encoding", resp.Header().Get("Content-Encoding")) - - length, err := strconv.ParseInt(l, 10, 64) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - - hrs := proxy.NewBufferedHttpReadSeeker(128*1024, m.Base.Url, + hrs := proxy.NewBufferedHttpReadSeeker(256*1024, m.Base.Url, proxy.WithContext(ctx), proxy.WithHeaders(m.Base.Headers), - proxy.WithContentLength(length), ) - name := resp.Header().Get("Content-Disposition") - if name == "" { - name = filepath.Base(resp.Request.RawRequest.URL.Path) - } else { - ctx.Header("Content-Disposition", name) - } - http.ServeContent(ctx.Writer, ctx.Request, name, time.Now(), hrs) + http.ServeContent(ctx.Writer, ctx.Request, m.Base.Url, time.Now(), hrs) } type FormatErrNotSupportFileType string @@ -532,152 +492,34 @@ func JoinLive(ctx *gin.Context) { } } -func ProxyVendorMovie(ctx *gin.Context, m *dbModel.Movie) { - if m.Base.VendorInfo.Vendor == "" { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor is empty")) - return - } - - switch m.Base.VendorInfo.Vendor { - case dbModel.StreamingVendorBilibili: - bvidI := m.Base.VendorInfo.Info["bvid"] - epIdI := m.Base.VendorInfo.Info["epId"] - if bvidI != nil && epIdI != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("bvid(%v) and epId(%v) can't be used at the same time", bvidI, epIdI))) - return - } - - var ( - bvid string - epId float64 - ok bool - ) - - if bvidI != nil { - bvid, ok = bvidI.(string) - if !ok { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("bvid is not string")) - return - } - } else if epIdI != nil { - epId, ok = epIdI.(float64) - if !ok { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("epId is not number")) - return - } - } else { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("bvid and epId is empty")) - return - } - - vendor, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(m.CreatorID, dbModel.StreamingVendorBilibili) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - cli := bilibili.NewClient(vendor.Cookies) - - var mu *bilibili.VideoURL - if bvid != "" { - cidI := m.Base.VendorInfo.Info["cid"] - if cidI == nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cid is empty")) - return - } - cid, ok := cidI.(float64) - if !ok { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cid is not number")) - return - } - mu, err = cli.GetVideoURL(0, bvid, uint(cid)) - } else { - mu, err = cli.GetPGCURL(uint(epId), 0) - } - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - - hrs := proxy.NewBufferedHttpReadSeeker(128*1024, mu.URL, - proxy.WithContext(ctx), - proxy.WithAppendHeaders(map[string]string{ - "Referer": "https://www.bilibili.com/", - "User-Agent": utils.UA, - }), - ) - http.ServeContent(ctx.Writer, ctx.Request, mu.URL, time.Now(), hrs) - - default: - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support")) - return - } -} - -func parse2VendorMovie(user *op.User, movie *dbModel.Movie) (err error) { - if movie.Base.Proxy { - return nil +func parse2VendorMovie(userID string, movie *dbModel.Movie) (err error) { + if movie.Base.VendorInfo.Shared { + userID = movie.CreatorID } switch movie.Base.VendorInfo.Vendor { case dbModel.StreamingVendorBilibili: - bvidI := movie.Base.VendorInfo.Info["bvid"] - epIdI := movie.Base.VendorInfo.Info["epId"] - if bvidI != nil && epIdI != nil { - return fmt.Errorf("bvid(%v) and epId(%v) can't be used at the same time", bvidI, epIdI) - } + info := movie.Base.VendorInfo.BilibiliVendorInfo - var ( - bvid string - ok bool - ) - - if bvidI != nil { - bvid, ok = bvidI.(string) - if !ok { - return fmt.Errorf("bvid is not string") - } - } else if epIdI != nil { - _, ok = epIdI.(float64) - if !ok { - return fmt.Errorf("epId is not number") - } - return - } else { - return fmt.Errorf("bvid and epId is empty") - } - - id := user.ID - if movie.Base.VendorInfo.Shared { - id = movie.CreatorID - } - vendor, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(id, dbModel.StreamingVendorBilibili) + vendor, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(userID, dbModel.StreamingVendorBilibili) if err != nil { return err } cli := bilibili.NewClient(vendor.Cookies) - var qn float64 = float64(bilibili.Q1080PP) - qnI, ok := movie.Base.VendorInfo.Info["qn"] - if ok { - qn, ok = qnI.(float64) - if !ok { - return fmt.Errorf("qn is not number") - } - } - cidI := movie.Base.VendorInfo.Info["cid"] - if cidI == nil { - return fmt.Errorf("cid is empty") - } - cid, ok := cidI.(float64) - if !ok { - return fmt.Errorf("cid is not number") + var mu *bilibili.VideoURL + if info.Bvid != "" { + mu, err = cli.GetVideoURL(0, info.Bvid, info.Cid, bilibili.WithQuality(info.Quality)) + } else if info.Epid != 0 { + mu, err = cli.GetPGCURL(info.Epid, 0, bilibili.WithQuality(info.Quality)) + } else { + err = errors.New("bvid and epid are empty") } - - mu, err := cli.GetVideoURL(0, bvid, uint(cid), bilibili.WithQuality(uint(qn))) if err != nil { return err } movie.Base.Url = mu.URL + return nil default: diff --git a/server/model/movie.go b/server/model/movie.go index 7551927..1cd32d9 100644 --- a/server/model/movie.go +++ b/server/model/movie.go @@ -41,10 +41,6 @@ func (p *PushMovieReq) Validate() error { return ErrTypeTooLong } - if p.VendorInfo.Vendor != "" && p.VendorInfo.Info == nil { - return errors.New("vendor info is empty") - } - return nil } diff --git a/vendors/bilibili/movie.go b/vendors/bilibili/movie.go index 79c3060..0763279 100644 --- a/vendors/bilibili/movie.go +++ b/vendors/bilibili/movie.go @@ -8,17 +8,18 @@ import ( ) type VideoPageInfo struct { - Bvid string `json:"bvid"` Title string `json:"title"` CoverImage string `json:"coverImage"` + Actors string `json:"actors"` VideoInfos []*VideoInfo `json:"videoInfos"` } type VideoInfo struct { - Cid int `json:"cid"` - // 分P + Bvid string `json:"bvid,omitempty"` + Cid int `json:"cid,omitempty"` + Epid uint `json:"epid,omitempty"` Name string `json:"name"` - FirstFrame string `json:"firstFrame"` + CoverImage string `json:"coverImage"` } func (c *Client) ParseVideoPage(aid uint, bvid string) (*VideoPageInfo, error) { @@ -46,16 +47,17 @@ func (c *Client) ParseVideoPage(aid uint, bvid string) (*VideoPageInfo, error) { } // TODO: error message r := &VideoPageInfo{ - Bvid: info.Data.Bvid, Title: info.Data.Title, CoverImage: info.Data.Pic, + Actors: info.Data.Owner.Name, + VideoInfos: make([]*VideoInfo, 0, len(info.Data.Pages)), } - r.VideoInfos = make([]*VideoInfo, 0, len(info.Data.Pages)) for _, page := range info.Data.Pages { r.VideoInfos = append(r.VideoInfos, &VideoInfo{ + Bvid: info.Data.Bvid, Cid: page.Cid, Name: page.Part, - FirstFrame: page.FirstFrame, + CoverImage: page.FirstFrame, }) } return r, nil @@ -170,20 +172,7 @@ func (c *Client) GetSubtitles(aid uint, bvid string, cid uint) ([]*Subtitle, err return r, nil } -type PGCPageInfo struct { - Actors string `json:"actors"` - CoverImage string `json:"coverImage"` - PGCInfos []*PGCInfo `json:"pgcInfos"` -} - -type PGCInfo struct { - EpId uint `json:"epId"` - Cid uint `json:"cid"` - Name string `json:"name"` - CoverImage string `json:"coverImage"` -} - -func (c *Client) ParsePGCPage(epId, season_id uint) (*PGCPageInfo, error) { +func (c *Client) ParsePGCPage(epId, season_id uint) (*VideoPageInfo, error) { var url string if epId != 0 { url = fmt.Sprintf("https://api.bilibili.com/pgc/view/web/season?ep_id=%d", epId) @@ -208,16 +197,16 @@ func (c *Client) ParsePGCPage(epId, season_id uint) (*PGCPageInfo, error) { return nil, err } - r := &PGCPageInfo{ - Actors: info.Result.Actors, + r := &VideoPageInfo{ + Title: info.Result.Title, CoverImage: info.Result.Cover, - PGCInfos: make([]*PGCInfo, len(info.Result.Episodes)), + Actors: info.Result.Actors, + VideoInfos: make([]*VideoInfo, len(info.Result.Episodes)), } for i, v := range info.Result.Episodes { - r.PGCInfos[i] = &PGCInfo{ - EpId: v.EpID, - Cid: v.Cid, + r.VideoInfos[i] = &VideoInfo{ + Epid: v.EpID, Name: v.ShareCopy, CoverImage: v.Cover, }