You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/vendors/bilibili/movie.go

558 lines
14 KiB
Go

package bilibili
import (
"errors"
"fmt"
"net/http"
"time"
json "github.com/json-iterator/go"
"github.com/zencoder/go-dash/v3/mpd"
)
type VideoPageInfo struct {
Title string `json:"title"`
Actors string `json:"actors"`
VideoInfos []*VideoInfo `json:"videoInfos"`
}
type VideoInfo struct {
Bvid string `json:"bvid,omitempty"`
Cid uint `json:"cid,omitempty"`
Epid uint `json:"epid,omitempty"`
Name string `json:"name"`
CoverImage string `json:"coverImage"`
}
type ParseVideoPageConf struct {
GetSections bool
}
type ParseVideoPageConfig func(*ParseVideoPageConf)
func WithGetSections(GetSections bool) ParseVideoPageConfig {
return func(c *ParseVideoPageConf) {
c.GetSections = GetSections
}
}
func (c *Client) ParseVideoPage(aid uint, bvid string, conf ...ParseVideoPageConfig) (*VideoPageInfo, error) {
config := &ParseVideoPageConf{}
for _, v := range conf {
v(config)
}
var url string
if aid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/x/web-interface/view?aid=%d", aid)
} else if bvid != "" {
url = fmt.Sprintf("https://api.bilibili.com/x/web-interface/view?bvid=%s", bvid)
} else {
return nil, fmt.Errorf("aid and bvid are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := videoPageInfo{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
r := &VideoPageInfo{
Title: info.Data.Title,
Actors: info.Data.Owner.Name,
}
if config.GetSections && len(info.Data.UgcSeason.Sections) != 0 {
r.Title = info.Data.UgcSeason.Title
for _, v := range info.Data.UgcSeason.Sections {
for _, episode := range v.Episodes {
r.VideoInfos = append(r.VideoInfos, &VideoInfo{
Bvid: episode.Bvid,
Cid: episode.Cid,
Name: episode.Title,
CoverImage: episode.Arc.Pic,
})
}
}
} else {
r.VideoInfos = make([]*VideoInfo, len(info.Data.Pages))
if len(info.Data.Pages) == 1 {
info.Data.Pages[0].Part = info.Data.Title
}
for i, page := range info.Data.Pages {
r.VideoInfos[i] = &VideoInfo{
Bvid: info.Data.Bvid,
Cid: page.Cid,
Name: page.Part,
CoverImage: info.Data.Pic,
}
}
}
return r, nil
}
const (
Q240P uint = 6
Q360P uint = 16
Q480P uint = 32
Q720P uint = 64
Q1080P uint = 80
Q1080PP uint = 112
Q1080P60 uint = 116
Q4K uint = 120
QHDR uint = 124
QDOLBY uint = 126
Q8K uint = 127
)
type VideoURL struct {
AcceptDescription []string `json:"acceptDescription"`
AcceptQuality []uint `json:"acceptQuality"`
CurrentQuality uint `json:"currentQuality"`
URL string `json:"url"`
}
type GetVideoURLConf struct {
Quality uint
}
func (c *GetVideoURLConf) fix() {
if c.Quality == 0 {
c.Quality = Q1080PP
}
}
type GetVideoURLConfig func(*GetVideoURLConf)
func WithQuality(q uint) GetVideoURLConfig {
return func(c *GetVideoURLConf) {
c.Quality = q
}
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md
func (c *Client) GetVideoURL(aid uint, bvid string, cid uint, conf ...GetVideoURLConfig) (*VideoURL, error) {
config := &GetVideoURLConf{
Quality: Q1080PP,
}
for _, v := range conf {
v(config)
}
config.fix()
var url string
if aid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?aid=%d&cid=%d&qn=%d&platform=html5&high_quality=1", aid, cid, config.Quality)
} else if bvid != "" {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?bvid=%s&cid=%d&qn=%d&platform=html5&high_quality=1", bvid, cid, config.Quality)
} else {
return nil, fmt.Errorf("aid and bvid are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := videoInfo{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
return &VideoURL{
AcceptDescription: info.Data.AcceptDescription,
AcceptQuality: info.Data.AcceptQuality,
CurrentQuality: info.Data.Quality,
URL: info.Data.Durl[0].URL,
}, nil
}
type GetDashVideoURLConf struct {
HDR bool
Need4K bool
NeedDOLBY bool
NeedDOLBYAudio bool
Need8K bool
NeedAV1 bool
}
type GetDashVideoURLConfig func(*GetDashVideoURLConf)
func WithHDR(hdr bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.HDR = hdr
}
}
func WithNeed4K(need4k bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.Need4K = need4k
}
}
func WithNeedDOLBY(needDOLBY bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.NeedDOLBY = needDOLBY
}
}
func WithNeedDOLBYAudio(needDOLBYAudio bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.NeedDOLBYAudio = needDOLBYAudio
}
}
func WithNeed8K(need8k bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.Need8K = need8k
}
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md
func (c *Client) GetDashVideoURL(aid uint, bvid string, cid uint, conf ...GetDashVideoURLConfig) (*mpd.MPD, error) {
config := &GetDashVideoURLConf{}
for _, v := range conf {
v(config)
}
var (
fnval uint = 16
extQuery string
)
if config.Need4K {
fnval = 128
extQuery = "&fourk=1"
} else if config.Need8K {
fnval = 1024
}
if config.HDR {
fnval |= 64
}
if config.NeedDOLBY {
fnval |= 512
}
if config.NeedDOLBYAudio {
fnval |= 256
}
if config.NeedAV1 {
fnval |= 2048
}
var url string
if aid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?aid=%d&cid=%d&fnver=0&platform=pc&fnval=%d%s", aid, cid, fnval, extQuery)
} else if bvid != "" {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?bvid=%s&cid=%d&fnver=0&platform=pc&fnval=%d%s", bvid, cid, fnval, extQuery)
} else {
return nil, fmt.Errorf("aid and bvid are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := dashResp{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
m := mpd.NewMPD(mpd.DASH_PROFILE_ONDEMAND, fmt.Sprintf("PT%.2fS", info.Data.Dash.Duration), fmt.Sprintf("PT%.2fS", info.Data.Dash.MinBufferTime))
var as *mpd.AdaptationSet
for _, v := range info.Data.Dash.Video {
as, err = m.AddNewAdaptationSetVideo(v.MimeType, "progressive", true, v.StartWithSap)
if err != nil {
return nil, err
}
video, err := as.AddNewRepresentationVideo(v.Bandwidth, v.Codecs, fmt.Sprint(time.Now().UnixMicro()), v.FrameRate, v.Width, v.Height)
if err != nil {
return nil, err
}
video.Sar = &v.Sar
err = video.AddNewBaseURL(v.BaseURL)
if err != nil {
return nil, err
}
_, err = video.AddNewSegmentBase(v.SegmentBase.IndexRange, v.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
as = nil
for _, a := range info.Data.Dash.Audio {
as, err = m.AddNewAdaptationSetAudio(a.MimeType, true, a.StartWithSap, "und")
if err != nil {
return nil, err
}
audio, err := as.AddNewRepresentationAudio(44100, a.Bandwidth, a.Codecs, fmt.Sprint(time.Now().UnixMicro()))
if err != nil {
return nil, err
}
audio.Sar = &a.Sar
err = audio.AddNewBaseURL(a.BaseURL)
if err != nil {
return nil, err
}
_, err = audio.AddNewSegmentBase(a.SegmentBase.IndexRange, a.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
return m, nil
}
type Subtitle struct {
Name string `json:"name"`
URL string `json:"url"`
}
func (c *Client) GetSubtitles(aid uint, bvid string, cid uint) ([]*Subtitle, error) {
var url string
if aid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/x/player/v2?aid=%d&cid=%d", aid, cid)
} else if bvid != "" {
url = fmt.Sprintf("https://api.bilibili.com/x/player/v2?bvid=%s&cid=%d", bvid, cid)
} else {
return nil, fmt.Errorf("aid and bvid are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := playerV2Info{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
r := make([]*Subtitle, len(info.Data.Subtitle.Subtitles))
for i, s := range info.Data.Subtitle.Subtitles {
r[i] = &Subtitle{
Name: s.LanDoc,
URL: s.SubtitleURL,
}
}
return r, nil
}
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)
} else if season_id != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/view/web/season?season_id=%d", season_id)
} else {
return nil, fmt.Errorf("edId and season_id are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := seasonInfo{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
r := &VideoPageInfo{
Title: info.Result.Title,
Actors: info.Result.Actors,
VideoInfos: make([]*VideoInfo, len(info.Result.Episodes)),
}
for i, v := range info.Result.Episodes {
r.VideoInfos[i] = &VideoInfo{
Epid: v.EpID,
Name: v.ShareCopy,
CoverImage: v.Cover,
}
}
return r, nil
}
func (c *Client) GetPGCURL(ep_id, cid uint, conf ...GetVideoURLConfig) (*VideoURL, error) {
config := &GetVideoURLConf{
Quality: Q1080PP,
}
for _, v := range conf {
v(config)
}
config.fix()
var url string
if ep_id != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?ep_id=%d&qn=%d&fourk=1&fnval=0", ep_id, config.Quality)
} else if cid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?cid=%d&qn=%d&fourk=1&fnval=0", cid, config.Quality)
} else {
return nil, fmt.Errorf("edId and season_id are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := pgcURLInfo{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
return &VideoURL{
AcceptDescription: info.Result.AcceptDescription,
AcceptQuality: info.Result.AcceptQuality,
CurrentQuality: info.Result.Quality,
URL: info.Result.Durl[0].URL,
}, nil
}
func (c *Client) GetDashPGCURL(ep_id, cid uint, conf ...GetDashVideoURLConfig) (*mpd.MPD, error) {
config := &GetDashVideoURLConf{}
for _, v := range conf {
v(config)
}
var (
fnval uint = 16
extQuery string
)
if config.Need4K {
fnval = 128
extQuery = "&fourk=1"
} else if config.Need8K {
fnval = 1024
}
if config.HDR {
fnval |= 64
}
if config.NeedDOLBY {
fnval |= 512
}
if config.NeedDOLBYAudio {
fnval |= 256
}
if config.NeedAV1 {
fnval |= 2048
}
var url string
if ep_id != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?ep_id=%d&fnval=%d%s", ep_id, fnval, extQuery)
} else if cid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?cid=%d&fnval=%d%s", ep_id, fnval, extQuery)
} else {
return nil, fmt.Errorf("edId and season_id are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := dashPGCResp{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
m := mpd.NewMPD(mpd.DASH_PROFILE_ONDEMAND, fmt.Sprintf("PT%.2fS", info.Result.Dash.Duration), fmt.Sprintf("PT%.2fS", info.Result.Dash.MinBufferTime))
var as *mpd.AdaptationSet
for _, v := range info.Result.Dash.Video {
as, err = m.AddNewAdaptationSetVideo(v.MimeType, "progressive", true, v.StartWithSap)
if err != nil {
return nil, err
}
video, err := as.AddNewRepresentationVideo(v.Bandwidth, v.Codecs, fmt.Sprint(time.Now().UnixMicro()), v.FrameRate, v.Width, v.Height)
if err != nil {
return nil, err
}
video.Sar = &v.Sar
err = video.AddNewBaseURL(v.BaseURL)
if err != nil {
return nil, err
}
_, err = video.AddNewSegmentBase(v.SegmentBase.IndexRange, v.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
as = nil
for _, a := range info.Result.Dash.Audio {
as, err = m.AddNewAdaptationSetAudio(a.MimeType, true, a.StartWithSap, "und")
if err != nil {
return nil, err
}
audio, err := as.AddNewRepresentationAudio(44100, a.Bandwidth, a.Codecs, fmt.Sprint(time.Now().UnixMicro()))
if err != nil {
return nil, err
}
audio.Sar = &a.Sar
err = audio.AddNewBaseURL(a.BaseURL)
if err != nil {
return nil, err
}
_, err = audio.AddNewSegmentBase(a.SegmentBase.IndexRange, a.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
return m, nil
}