mirror of https://github.com/synctv-org/synctv
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.
558 lines
14 KiB
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
|
|
}
|