用go语言实现B站视频下载器
方法
1. 获取cid
对于单个视频,这个接口获得的是这个视频的cid
对于视频合集,这个接口获得的是所有视频的cid
列表
注:一个视频对应一个cid
https://api.bilibili.com/x/player/pagelist?bvid=bvid
2. 获取每个视频的视频文件URL
对于第1步拿到的每一个cid
调用如下接口,可以获得每个cid
的视频文件URL
其中qn
是视频的质量,取值对应列表如下:
120 : "超清 4K"
116 : "高清1080P60"
112 : "高清1080P+"
80 : "1080P"
74 : "高清720P60"
64 : "高清720P"
32 : "清晰480P"
16 : "流畅360P"
特别的,如果要下载4K
视频需要添加URL参数fourk=1
https://api.bilibili.com/x/player/playurl?bvid=bvid&cid=cid&qn=qn
代码
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
var qnMap = map[string]struct {
QN int
NeedCookie bool
Detail string
}{
"4K": {120, true, "超清 4K"},
"1080P60": {116, true, "高清1080P60"},
"1080P+": {112, true, "高清1080P+"},
"1080P": {80, true, "1080P"},
"720P60": {74, true, "高清720P60"},
"720P": {64, false, "高清720P"},
"480P": {32, false, "清晰480P"},
"360P": {16, false, "流畅360P"},
}
type bilibiliCid struct {
Bvid string
Cid string
Title string
QN int
PlayURLs []string
}
var (
bvid string
page int // 视频合集的分p, 如果不指定,默认为0表示全下
dir string
qn int
sessionData string
)
func init() {
var _qn string
flag.StringVar(&bvid, "b", "", "bvid with BV prefix in the url")
flag.IntVar(&page, "p", 0, "single video page number of video list")
flag.StringVar(&dir, "d", "./", "which directory to save")
flag.StringVar(&_qn, "q", "1080P60", "video quality")
flag.StringVar(&sessionData, "s", "", "value of your bilibili cookie[\"SESSDATA\"]")
flag.Parse()
_qn = strings.ToUpper(_qn)
if bvid == "" {
log.Fatalf("you must specify bvid")
}
if v, ok := qnMap[_qn]; ok {
qn = v.QN
if v.NeedCookie && sessionData == "" {
log.Fatalf("need set value of bilibili cookie[\"SESSDATA\"] when you download %v", v.Detail)
}
} else {
log.Fatalf("invalid qn value")
}
}
func main() {
if bvid == "" {
log.Fatalf("invalid bvid\n")
}
log.Printf("%v@%v\n", bvid, qn)
videos := getCidList(bvid, qn)
for i, v := range videos {
if page == 0 || page == (i+1) {
v.download(dir)
}
}
}
type downloader struct {
io.Reader
Total int64
Current int64
}
func (d *downloader) Read(p []byte) (n int, err error) {
n, err = d.Reader.Read(p)
d.Current += int64(n)
fmt.Printf("\rprogress: %.2f%%", float64(d.Current)*100.0/float64(d.Total))
return
}
func (c *bilibiliCid) download(dir string) {
for i, URL := range c.PlayURLs {
u, err := url.Parse(URL)
if err != nil {
log.Printf("ERR: %v", err)
continue
}
name := fmt.Sprintf("%v_%v_%v", c.Bvid, c.Title, path.Base(path.Base(u.Path)))
log.Printf("Downloading[%d]: name:%v\n\turl:%v\n", i, name, URL)
client := &http.Client{}
req, err := http.NewRequest("GET", URL, nil)
if err != nil {
log.Println(err)
return
}
setUserAgent(req)
setCookie(req)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Range", "bytes=0-") // Range 的值要为 bytes=0- 才能下载完整视频
req.Header.Set("Referer", "https://www.bilibili.com/video/"+bvid) // 必需添加
req.Header.Set("Origin", "https://www.bilibili.com")
req.Header.Set("Connection", "keep-alive")
rsp, err := client.Do(req)
if err != nil {
log.Println(err)
return
}
defer rsp.Body.Close()
path := filepath.Join(dir, name)
log.Printf("save to: %v", path)
out, err := os.Create(path)
if err != nil {
log.Printf("err: %v", err)
continue
}
defer out.Close()
dr := &downloader{
rsp.Body,
rsp.ContentLength,
0,
}
io.Copy(out, dr)
fmt.Println("")
}
}
func (c *bilibiliCid) getPlayURLs() {
url := fmt.Sprintf("https://api.bilibili.com/x/player/playurl?bvid=%v&cid=%v&qn=%v&fourk=1", c.Bvid, c.Cid, c.QN)
fmt.Println(url)
pl := struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Quality int `json:"quality"`
Durl []struct {
Order int `json:"order"`
URL string `json:"url"`
BackupURL string `json:"backup_url"`
} `json:"durl"`
} `json:"data"`
}{}
data := rawGetURL(url, setCookie)
fmt.Println(data)
json.Unmarshal([]byte(data), &pl)
for i, p := range pl.Data.Durl {
log.Printf("PlayList[%d]: quality %v order %v url %v %v", i, pl.Data.Quality, p.Order, p.URL, p.BackupURL)
c.PlayURLs = append(c.PlayURLs, p.URL)
}
}
func getCidList(bvid string, qn int) []bilibiliCid {
cl := struct {
Code int `json:"code"`
Message string `json:"message"`
Data []struct {
Cid int64 `json:"cid"`
Page int `json:"page"`
Part string `json:"part"`
Duration int `json:"duration"`
Vid string `json:"vid"`
Dimension struct {
Width int `json:"width"`
Height int `json:"height"`
Rotate int `json:"rotate"`
} `json:"Dimension"`
} `json:"data"`
}{}
data := getURL("https://api.bilibili.com/x/player/pagelist?bvid=" + bvid)
json.Unmarshal([]byte(data), &cl)
if len(cl.Data) == 0 {
log.Printf("ERR: get cid list failed")
}
var cids []bilibiliCid
for i, d := range cl.Data {
c := bilibiliCid{}
c.Cid = strconv.FormatInt(d.Cid, 10)
c.Title = d.Part
c.Bvid = bvid
c.QN = qn
c.getPlayURLs()
cids = append(cids, c)
log.Printf("CidList[%d]: %v %v %v %v", i, d.Cid, d.Part, d.Dimension.Width, d.Dimension.Height)
}
return cids
}
func setUserAgent(req *http.Request) {
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36")
}
func setCookie(req *http.Request) {
cookie := http.Cookie{Name: "SESSDATA", Value: sessionData, Expires: time.Now().Add(30 * 24 * 60 * 60 * time.Second)}
log.Printf("got bilibili cookie, SESSDATA:%v", sessionData)
req.AddCookie(&cookie)
}
func getURL(url string) string {
return rawGetURL(url, nil)
}
func rawGetURL(url string, headerSet func(*http.Request)) (s string) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Println(err)
return
}
setUserAgent(req)
if headerSet != nil {
headerSet(req)
}
res, err := client.Do(req)
if err != nil {
log.Println(err)
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
log.Printf("http return %v\n", res.StatusCode)
return
}
rsp, _ := ioutil.ReadAll(res.Body)
s = string(rsp)
return
}
补充
获取B站视频弹幕的方法
只需要上文提到获取到的cid
,就可以直接请求到弹幕内容
http://comment.bilibili.com/cid.xml