├── C └── C.go ├── README.md ├── api ├── api.go ├── api_test.go ├── wbi.go └── wbi_test.go ├── go.mod ├── go.sum ├── main.go └── util └── limit.go /C/C.go: -------------------------------------------------------------------------------- 1 | package C 2 | 3 | var ( 4 | Cookie string 5 | UP string 6 | O string 7 | FFMPEG bool 8 | WD string 9 | J int 10 | BVs string 11 | Merge bool 12 | Delete bool 13 | Debug bool 14 | AddBVSuffix bool 15 | DisableOverwrite bool 16 | ) 17 | 18 | const ( 19 | GetAllBV = `Array.from(document.querySelectorAll('#submit-video-list > ul.clearfix.cube-list > li')).map(e=>e.dataset['aid']).join(',')` 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bili-dl 2 | ## 安装 3 | ``` shell 4 | go install github.com/yu1745/bili-dl@latest 5 | ``` 6 | 7 | ## 注意 8 | 9 | * 要想下载画质高于480P的视频请指定cookie, cookie获取方式为用浏览器登录b站后,按F12打开控制台,点击右上角加号,选择"应用"或"Application",选择存储,选择Cookie,选择www.bilibili.com然后找到名称是SESSDATA那一行,将值复制出来 10 | * 需要环境变量中有ffmpeg,软件使用dash的方式取流,取得的音视频流是分开的,需要调用ffmpeg合并 11 | 12 | ## 功能 13 | 14 | 下载b站视频,支持批量下载,支持指定cookie实现高画质视频下载 15 | 16 | ``` shell 17 | -bv string 18 | 单或多个bv号, 多个时用逗号分隔, 如: "BVxxxxxx,BVyyyyyyy" 19 | 可以通过在浏览器控制台输入以下代码来获取整页的BV 20 | Array.from(document.querySelectorAll('#submit-video-list > ul.clearfix.cube-list > li')).map(e=>e.dataset['aid']).join(',') 21 | -c string 22 | cookie,cookie的key是SESSDATA,不设置只能下载清晰度小于等于480P的视频 23 | -d 合并后是否删除单视频和单音频 (default true) 24 | -j int 25 | 同时下载的任务数 26 | 机械硬盘不应超过5 (default 1) 27 | -m 是否合并视频流和音频流, 不合并将得到单独的视频(不含音频)和单独的音频(不含视频)文件, 不利于正常播放 (default true) 28 | -no-overwrite 29 | 跳过下载过的视频 30 | 注意: 需要先前下载时没有指定suffix为false (default true) 31 | -o string 32 | 下载路径,可填相对或绝对路径,建议在windows下使用相对路径避免正反斜杠问题 (default ".") 33 | -suffix 34 | 在下载的视频文件名后添加bv号 35 | 用来解决视频重名问题 36 | 关闭后跳过已下载功能将失效 (default true) 37 | ``` 38 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | url2 "net/url" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "regexp" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | jsoniter "github.com/json-iterator/go" 18 | "github.com/yu1745/bili-dl/C" 19 | ) 20 | 21 | var client = &http.Client{ 22 | Transport: &http.Transport{ 23 | Proxy: http.ProxyFromEnvironment, 24 | //禁止复用连接,防止同一个连接长时间大流量被限速 25 | DisableKeepAlives: true, 26 | }, 27 | } 28 | 29 | func videoInfo(bv string) ([]byte, error) { 30 | url := "https://api.bilibili.com/x/web-interface/view" 31 | parse, _ := url2.Parse(url) 32 | query := parse.Query() 33 | query.Add("bvid", bv) 34 | parse.RawQuery = query.Encode() 35 | url = parse.String() 36 | method := "GET" 37 | req, err := http.NewRequest(method, url, nil) 38 | 39 | if err != nil { 40 | log.Println(err) 41 | return nil, err 42 | } 43 | req.Header.Add("User-Agent", "Apifox/1.0.0 (https://www.apifox.cn)") 44 | 45 | res, err := client.Do(req) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer res.Body.Close() 50 | 51 | body, err := io.ReadAll(res.Body) 52 | if err != nil { 53 | log.Println(err) 54 | return nil, err 55 | } 56 | //log.Println(string(body)) 57 | return body, nil 58 | } 59 | 60 | func ResolveVideo(v *Video) (*Video, error) { 61 | info, err := videoInfo(v.BV) 62 | if err != nil { 63 | return nil, err 64 | } 65 | cid := jsoniter.Get(info, "data", "cid").ToString() 66 | title := jsoniter.Get(info, "data", "title").ToString() 67 | //v.BV = bv 68 | v.Cid = cid 69 | v.Title = title 70 | return v, nil 71 | } 72 | 73 | func videoFromUP(mid string, pn int) (rt []byte, err error) { 74 | url := "https://api.bilibili.com/x/space/wbi/arc/search?order=pubdate&ps=49" 75 | parse, _ := url2.Parse(url) 76 | query := parse.Query() 77 | query.Add("mid", mid) 78 | query.Add("pn", strconv.Itoa(pn)) 79 | parse.RawQuery = query.Encode() 80 | url = parse.String() 81 | url, err = sign(url) 82 | if err != nil { 83 | return nil, err 84 | } 85 | method := "GET" 86 | 87 | req, err := http.NewRequest(method, url, nil) 88 | 89 | if err != nil { 90 | log.Println(err) 91 | return nil, err 92 | } 93 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0") 94 | req.Header.Set("Referer", "https://www.bilibili.com/") 95 | 96 | res, err := client.Do(req) 97 | if err != nil { 98 | log.Println(err) 99 | return nil, err 100 | } 101 | defer res.Body.Close() 102 | 103 | body, err := io.ReadAll(res.Body) 104 | if err != nil { 105 | log.Println(err) 106 | return nil, err 107 | } 108 | //log.Println(string(body)) 109 | return body, err 110 | } 111 | 112 | type Video struct { 113 | Title string `json:"title,omitempty"` 114 | BV string `json:"bv,omitempty"` 115 | Cid string `json:"cid,omitempty"` 116 | // Author string `json:"author,omitempty"` 117 | } 118 | 119 | func AllVideo(mid string) ([]Video, error) { 120 | bytes, err := videoFromUP(mid, 1) 121 | if err != nil { 122 | return nil, err 123 | } 124 | var videos []Video 125 | if C.Debug { 126 | log.Println(string(bytes)) 127 | } 128 | count := jsoniter.Get(bytes, "data", "page", "count").ToInt() 129 | var pn int 130 | n := 49 131 | if count%n == 0 { 132 | pn = count / n 133 | } else { 134 | pn = count/n + 1 135 | } 136 | for i := 1; i <= pn; i++ { 137 | time.Sleep(time.Second) 138 | bytes, err := videoFromUP(mid, i) 139 | if err != nil { 140 | return nil, err 141 | } 142 | vlist := jsoniter.Get(bytes, "data", "list", "vlist").ToString() 143 | var m []map[string]any 144 | err = jsoniter.Unmarshal([]byte(vlist), &m) 145 | if err != nil { 146 | return nil, err 147 | } 148 | for _, v := range m { 149 | if bvid := v["bvid"]; bvid != nil { 150 | //log.Println(bvid) 151 | /*info, err := videoInfo(bvid.(string)) 152 | if err != nil { 153 | return nil, err 154 | } 155 | cid := jsoniter.Get(info, "data", "cid").ToString() 156 | title := jsoniter.Get(info, "data", "title").ToString()*/ 157 | video := Video{BV: bvid.(string) /* Author: mid */ /*, Cid: cid, Title: title*/} 158 | //log.Printf("%+v\n", video) 159 | videoJson, err := jsoniter.MarshalToString(&video) 160 | if err != nil { 161 | return nil, err 162 | } 163 | println(videoJson) 164 | videos = append(videos, video) 165 | } 166 | } 167 | } 168 | return videos, nil 169 | } 170 | 171 | func codec2i(codec string) int { 172 | if strings.HasPrefix(codec, "avc") { 173 | return 1 174 | } else if strings.HasPrefix(codec, "hev") { 175 | return 2 176 | } else if strings.HasPrefix(codec, "av01") { 177 | return 3 178 | } else { 179 | return 0 180 | } 181 | } 182 | 183 | type Stream struct { 184 | V string 185 | A string 186 | Video 187 | } 188 | 189 | func GetStream(v Video) (*Stream, error) { 190 | url := "https://api.bilibili.com/x/player/wbi/playurl?fnver=0&fnval=3216&fourk=1&qn=127" 191 | parse, _ := url2.Parse(url) 192 | query := parse.Query() 193 | query.Add("bvid", v.BV) 194 | query.Add("cid", v.Cid) 195 | parse.RawQuery = query.Encode() 196 | url = parse.String() 197 | var err error 198 | url, err = sign(url) 199 | if err != nil { 200 | return nil, err 201 | } 202 | method := "GET" 203 | 204 | req, err := http.NewRequest(method, url, nil) 205 | if err != nil { 206 | return nil, err 207 | } 208 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0") 209 | req.Header.Set("Referer", "https://www.bilibili.com/") 210 | 211 | req.AddCookie(&http.Cookie{Name: "SESSDATA", Value: C.Cookie}) 212 | 213 | res, err := client.Do(req) 214 | if err != nil { 215 | return nil, err 216 | } 217 | defer res.Body.Close() 218 | 219 | body, err := io.ReadAll(res.Body) 220 | if err != nil { 221 | return nil, err 222 | } 223 | videos := jsoniter.Get(body, "data", "dash", "video").ToString() 224 | var l []map[string]any 225 | err = jsoniter.Unmarshal([]byte(videos), &l) 226 | if err != nil { 227 | return nil, err 228 | } 229 | sort.Slice(l, func(i, j int) bool { 230 | if codec2i(l[i]["codecs"].(string)) == codec2i(l[j]["codecs"].(string)) { 231 | return l[i]["width"].(float64) > l[j]["width"].(float64) 232 | } else { 233 | return codec2i(l[i]["codecs"].(string)) > codec2i(l[j]["codecs"].(string)) 234 | } 235 | }) 236 | audios := jsoniter.Get(body, "data", "dash", "audio").ToString() 237 | var l2 []map[string]any 238 | err = jsoniter.Unmarshal([]byte(audios), &l2) 239 | if err != nil { 240 | return nil, err 241 | } 242 | sort.Slice(l2, func(i, j int) bool { 243 | return l2[i]["bandwidth"].(float64) > l2[j]["bandwidth"].(float64) 244 | }) 245 | stream := &Stream{V: l[0]["base_url"].(string), A: l2[0]["base_url"].(string), Video: v} 246 | return stream, nil 247 | } 248 | 249 | func Dl(stream *Stream) error { 250 | stream.Title = fileNameFix(stream.Title) 251 | err := DV(stream) 252 | if err != nil { 253 | return err 254 | } 255 | err = DA(stream) 256 | if err != nil { 257 | return err 258 | } 259 | log.Println(stream.Title, "下载完成") 260 | return nil 261 | } 262 | 263 | func DV(stream *Stream) error { 264 | req, err := http.NewRequest("GET", stream.V, nil) 265 | if err != nil { 266 | return err 267 | } 268 | req.Header.Set("Referer", "https://www.bilibili.com") 269 | resp, err := client.Do(req) 270 | if err != nil { 271 | return err 272 | } 273 | var file *os.File 274 | if C.AddBVSuffix { 275 | file, err = os.OpenFile(filepath.Join(C.O, stream.Title+"_"+stream.BV+".mp4"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) 276 | if err != nil { 277 | return err 278 | } 279 | } else { 280 | file, err = os.OpenFile(filepath.Join(C.O, stream.Title+".mp4"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) 281 | if err != nil { 282 | return err 283 | } 284 | } 285 | defer file.Close() 286 | _ = file.Truncate(0) 287 | _, err = io.Copy(file, resp.Body) 288 | if err != nil { 289 | return err 290 | } 291 | return nil 292 | } 293 | 294 | func DA(stream *Stream) error { 295 | req, err := http.NewRequest("GET", stream.A, nil) 296 | if err != nil { 297 | return err 298 | } 299 | req.Header.Set("Referer", "https://www.bilibili.com") 300 | resp, err := client.Do(req) 301 | if err != nil { 302 | return err 303 | } 304 | var file *os.File 305 | if C.AddBVSuffix { 306 | file, err = os.OpenFile(filepath.Join(C.O, stream.Title+"_"+stream.BV+".mp3"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) 307 | if err != nil { 308 | return err 309 | } 310 | } else { 311 | file, err = os.OpenFile(filepath.Join(C.O, stream.Title+".mp3"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) 312 | if err != nil { 313 | return err 314 | } 315 | } 316 | defer file.Close() 317 | _ = file.Truncate(0) 318 | _, err = io.Copy(file, resp.Body) 319 | if err != nil { 320 | return err 321 | } 322 | return nil 323 | } 324 | 325 | func VideoFromBV(bv string) (*Video, error) { 326 | info, err := videoInfo(bv) 327 | if err != nil { 328 | return nil, err 329 | } 330 | cid := jsoniter.Get(info, "data", "cid").ToString() 331 | title := jsoniter.Get(info, "data", "title").ToString() 332 | video := Video{BV: bv, Cid: cid, Title: title} 333 | log.Printf("%+v\n", video) 334 | return &video, nil 335 | } 336 | 337 | func Merge(stream *Stream) error { 338 | var video string 339 | var audio string 340 | var output string 341 | if C.AddBVSuffix { 342 | video = filepath.Join(C.O, stream.Title+"_"+stream.BV+".mp4") 343 | audio = filepath.Join(C.O, stream.Title+"_"+stream.BV+".mp3") 344 | output = filepath.Join(C.O, stream.Title+"_"+stream.BV+"-merged.mp4") 345 | } else { 346 | video = filepath.Join(C.O, stream.Title+".mp4") 347 | audio = filepath.Join(C.O, stream.Title+".mp3") 348 | output = filepath.Join(C.O, stream.Title+"-merged.mp4") 349 | } 350 | cmd := exec.Command("ffmpeg", "-y", "-i", video, "-i", audio, "-c", "copy", output) 351 | err := cmd.Run() 352 | if err != nil { 353 | log.Println(err) 354 | } 355 | if C.Delete { 356 | err := os.Remove(video) 357 | if err != nil { 358 | return err 359 | } 360 | err = os.Remove(audio) 361 | if err != nil { 362 | return err 363 | } 364 | // err = os.Rename(filepath.Join(C.O, stream.Title+"-merged.mp4"), filepath.Join(C.O, stream.Title+".mp4")) 365 | if C.AddBVSuffix { 366 | err = os.Rename(output, filepath.Join(C.O, stream.Title+"_"+stream.BV+".mp4")) 367 | if err != nil { 368 | return err 369 | } 370 | } else { 371 | err = os.Rename(output, filepath.Join(C.O, stream.Title+".mp4")) 372 | if err != nil { 373 | return err 374 | } 375 | } 376 | 377 | } 378 | log.Println(stream.Title, "合并完成") 379 | return nil 380 | } 381 | 382 | var reg = regexp.MustCompile(`[/\\:*?"<>|]`) 383 | 384 | // 去掉文件名中的非法字符 385 | func fileNameFix(name string) string { 386 | return reg.ReplaceAllString(name, " ") 387 | } 388 | -------------------------------------------------------------------------------- /api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | jsoniter "github.com/json-iterator/go" 6 | "testing" 7 | ) 8 | 9 | var testVideo = Video{BV: "BV1uP4y1S7ps", Cid: "873198432", Title: "会有人不喜欢玛奇玛?硬撑罢了!"} 10 | 11 | func TestVideoInfo(t *testing.T) { 12 | bytes, err := videoInfo("BV1uP4y1S7ps") 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | t.Log(jsoniter.Get(bytes, "data", "cid").ToString()) 17 | } 18 | 19 | func TestAllVideo(t *testing.T) { 20 | /*videos*/ _, err := AllVideo("2223018") 21 | if err != nil { 22 | t.Log(err) 23 | } 24 | //fmt.Printf("%+v\n", videos) 25 | } 26 | 27 | func TestStream(t *testing.T) { 28 | stream, err := GetStream(testVideo) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | fmt.Printf("%+v\n", *stream) 33 | } 34 | 35 | func TestDl(t *testing.T) { 36 | stream, err := GetStream(testVideo) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | err = Dl(stream) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | } 45 | 46 | func Test_fileNameFix(t *testing.T) { 47 | type args struct { 48 | name string 49 | } 50 | tests := []struct { 51 | name string 52 | args args 53 | want string 54 | }{ 55 | {"test", args{"a|a"}, "a a"}, 56 | {"test", args{"a:a"}, "a a"}, 57 | {"test", args{"a*a"}, "a a"}, 58 | {"test", args{"a?a"}, "a a"}, 59 | {"test", args{"aa"}, "a a"}, 61 | {"test", args{"a\"a"}, "a a"}, 62 | {"test", args{"a\\a"}, "a a"}, 63 | {"test", args{"a/a"}, "a a"}, 64 | {"test", args{"a|a:a*a?aa\"a\\a/a"}, "a a a a a a a a a a"}, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | if got := fileNameFix(tt.args.name); got != tt.want { 69 | t.Errorf("fileNameFix() = %v, want %v", got, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /api/wbi.go: -------------------------------------------------------------------------------- 1 | //copy from https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md#Golang and modified 2 | package api 3 | 4 | import ( 5 | "crypto/md5" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | jsoniter "github.com/json-iterator/go" 18 | ) 19 | 20 | var ( 21 | mixinKeyEncTab = []int{ 22 | 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 23 | 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 24 | 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 25 | 36, 20, 34, 44, 52, 26 | } 27 | cache sync.Map 28 | lastUpdateTime time.Time 29 | ) 30 | 31 | func main() { 32 | urlStr := "https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091" 33 | newUrlStr, err := sign(urlStr) 34 | if err != nil { 35 | fmt.Printf("Error: %s", err) 36 | return 37 | } 38 | req, err := http.NewRequest("GET", newUrlStr, nil) 39 | if err != nil { 40 | fmt.Printf("Error: %s", err) 41 | return 42 | } 43 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") 44 | response, err := http.DefaultClient.Do(req) 45 | if err != nil { 46 | fmt.Printf("Request failed: %s", err) 47 | return 48 | } 49 | defer response.Body.Close() 50 | body, err := io.ReadAll(response.Body) 51 | if err != nil { 52 | fmt.Printf("Failed to read response: %s", err) 53 | return 54 | } 55 | fmt.Println(string(body)) 56 | } 57 | 58 | func sign(urlStr string) (string, error) { 59 | urlObj, err := url.Parse(urlStr) 60 | if err != nil { 61 | return "", err 62 | } 63 | imgKey, subKey := getWbiKeysCached() 64 | query := urlObj.Query() 65 | params := map[string]string{} 66 | for k, v := range query { 67 | params[k] = v[0] 68 | } 69 | newParams := encWbi(params, imgKey, subKey) 70 | for k, v := range newParams { 71 | query.Set(k, v) 72 | } 73 | urlObj.RawQuery = query.Encode() 74 | newUrlStr := urlObj.String() 75 | return newUrlStr, nil 76 | } 77 | 78 | func encWbi(params map[string]string, imgKey, subKey string) map[string]string { 79 | mixinKey := getMixinKey(imgKey + subKey) 80 | currTime := strconv.FormatInt(time.Now().Unix(), 10) 81 | params["wts"] = currTime 82 | 83 | // Sort keys 84 | keys := make([]string, 0, len(params)) 85 | for k := range params { 86 | keys = append(keys, k) 87 | } 88 | sort.Strings(keys) 89 | 90 | // Remove unwanted characters 91 | for k, v := range params { 92 | v = sanitizeString(v) 93 | params[k] = v 94 | } 95 | 96 | // Build URL parameters 97 | query := url.Values{} 98 | for _, k := range keys { 99 | query.Set(k, params[k]) 100 | } 101 | queryStr := query.Encode() 102 | 103 | // Calculate w_rid 104 | hash := md5.Sum([]byte(queryStr + mixinKey)) 105 | params["w_rid"] = hex.EncodeToString(hash[:]) 106 | return params 107 | } 108 | 109 | func getMixinKey(orig string) string { 110 | var str strings.Builder 111 | for _, v := range mixinKeyEncTab { 112 | if v < len(orig) { 113 | str.WriteByte(orig[v]) 114 | } 115 | } 116 | return str.String()[:32] 117 | } 118 | 119 | func sanitizeString(s string) string { 120 | unwantedChars := []string{"!", "'", "(", ")", "*"} 121 | for _, char := range unwantedChars { 122 | s = strings.ReplaceAll(s, char, "") 123 | } 124 | return s 125 | } 126 | 127 | func updateCache() { 128 | if time.Since(lastUpdateTime).Minutes() < 10 { 129 | return 130 | } 131 | imgKey, subKey := getWbiKeys() 132 | cache.Store("imgKey", imgKey) 133 | cache.Store("subKey", subKey) 134 | lastUpdateTime = time.Now() 135 | } 136 | 137 | func getWbiKeysCached() (string, string) { 138 | updateCache() 139 | imgKeyI, _ := cache.Load("imgKey") 140 | subKeyI, _ := cache.Load("subKey") 141 | return imgKeyI.(string), subKeyI.(string) 142 | } 143 | 144 | func getWbiKeys() (string, string) { 145 | resp, err := http.Get("https://api.bilibili.com/x/web-interface/nav") 146 | if err != nil { 147 | fmt.Printf("Error: %s", err) 148 | return "", "" 149 | } 150 | defer resp.Body.Close() 151 | body, err := io.ReadAll(resp.Body) 152 | if err != nil { 153 | fmt.Printf("Error: %s", err) 154 | return "", "" 155 | } 156 | json := string(body) 157 | imgURL := jsoniter.Get([]byte(json), "data", "wbi_img", "img_url").ToString() 158 | subURL := jsoniter.Get([]byte(json), "data", "wbi_img", "sub_url").ToString() 159 | imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0] 160 | subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0] 161 | return imgKey, subKey 162 | } 163 | -------------------------------------------------------------------------------- /api/wbi_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestWbi(t *testing.T) { 10 | urlStr := "https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091" 11 | newUrlStr, err := sign(urlStr) 12 | if err != nil { 13 | t.Errorf("Error: %s", err) 14 | return 15 | } 16 | req, err := http.NewRequest("GET", newUrlStr, nil) 17 | if err != nil { 18 | t.Errorf("Error: %s", err) 19 | return 20 | } 21 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") 22 | response, err := http.DefaultClient.Do(req) 23 | if err != nil { 24 | t.Errorf("Request failed: %s", err) 25 | return 26 | } 27 | defer response.Body.Close() 28 | body, err := io.ReadAll(response.Body) 29 | if err != nil { 30 | t.Errorf("Failed to read response: %s", err) 31 | return 32 | } 33 | t.Log(string(body)) 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yu1745/bili-dl 2 | 3 | go 1.19 4 | 5 | require github.com/json-iterator/go v1.1.12 6 | 7 | require ( 8 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 9 | github.com/modern-go/reflect2 v1.0.2 // indirect 10 | github.com/stretchr/testify v1.5.1 // indirect 11 | gopkg.in/yaml.v2 v2.4.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 5 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 6 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 7 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 8 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 9 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 10 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 16 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 20 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/fs" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "regexp" 12 | "runtime" 13 | "strings" 14 | 15 | "github.com/yu1745/bili-dl/C" 16 | "github.com/yu1745/bili-dl/api" 17 | "github.com/yu1745/bili-dl/util" 18 | ) 19 | 20 | func init() { 21 | log.SetFlags(log.Lshortfile) 22 | flag.StringVar(&C.Cookie, "c", "", "cookie,cookie的key是SESSDATA,不设置只能下载清晰度小于等于480P的视频") 23 | // flag.StringVar(&C.UP, "up", "", "up主id,设置后会下载该up主的所有视频") 24 | flag.StringVar(&C.O, "o", ".", "下载路径,可填相对或绝对路径,建议在windows下使用相对路径避免正反斜杠问题") 25 | flag.IntVar(&C.J, "j", 1, "同时下载的任务数\n机械硬盘不应超过5") 26 | flag.StringVar(&C.BVs, "bv", "", fmt.Sprintf("单或多个bv号, 多个时用逗号分隔, 如: \"BVxxxxxx,BVyyyyyyy\"\n可以通过在浏览器控制台输入以下代码来获取整页的BV\n%s", C.GetAllBV)) 27 | flag.BoolVar(&C.Merge, "m", true, "是否合并视频流和音频流, 不合并将得到单独的视频(不含音频)和单独的音频(不含视频)文件, 不利于正常播放") 28 | flag.BoolVar(&C.Delete, "d", true, "合并后是否删除单视频和单音频") 29 | // flag.BoolVar(&C.Debug, "debug", false, "是否打印调试信息") 30 | flag.BoolVar(&C.AddBVSuffix, "suffix", true, "在下载的视频文件名后添加bv号\n用来解决视频重名问题\n关闭后跳过已下载功能将失效") 31 | flag.BoolVar(&C.DisableOverwrite, "no-overwrite", true, "跳过下载过的视频\n注意: 需要先前下载时没有指定suffix为false") 32 | flag.Parse() 33 | C.WD, _ = os.Getwd() 34 | if //goland:noinspection GoBoolExpressions 35 | runtime.GOOS == "windows" || runtime.GOOS == "nt" { 36 | pattern := `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$` 37 | if matched, _ := regexp.MatchString(pattern, C.O); !matched { 38 | C.O = filepath.Join(C.WD, C.O) 39 | err := os.MkdirAll(C.O, os.ModePerm) 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | } else { 45 | if !strings.HasPrefix(C.O, "/") { 46 | C.O = filepath.Join(C.WD, C.O) 47 | err := os.MkdirAll(C.O, os.ModePerm) 48 | if err != nil { 49 | panic(err) 50 | } 51 | } 52 | } 53 | log.Println("下载路径: ", C.O) 54 | cmd := exec.Command("ffmpeg", "-version") 55 | //cmd.Stdout = os.Stdout 56 | err := cmd.Run() 57 | if err != nil { 58 | log.Println(err) 59 | log.Println("ffmpeg未找到,将不会合并音频和视频") 60 | } else { 61 | C.FFMPEG = true 62 | } 63 | reg := regexp.MustCompile(`.*_BV[a-zA-Z0-9]+\.mp4`) 64 | bvReg := regexp.MustCompile(`BV[a-zA-Z0-9]+`) 65 | bvs := make(map[string]struct{}) 66 | for _, v := range strings.Split(C.BVs, ",") { 67 | if v != "" { 68 | bvs[v] = struct{}{} 69 | } 70 | } 71 | exists := make(map[string]struct{}) 72 | if C.DisableOverwrite { 73 | err = filepath.WalkDir(C.O, func(path string, d fs.DirEntry, err error) error { 74 | if !d.IsDir() && reg.MatchString(d.Name()) { 75 | exists[bvReg.FindString(d.Name())] = struct{}{} 76 | } 77 | return nil 78 | }) 79 | if err != nil { 80 | log.Fatalln(err) 81 | } 82 | for k := range bvs { 83 | if _, ok := exists[k]; ok { 84 | log.Printf("%s已存在, 将不会下载\n", k) 85 | delete(bvs, k) 86 | } 87 | } 88 | keys := make([]string, 0, len(bvs)) 89 | for k := range bvs { 90 | keys = append(keys, k) 91 | } 92 | C.BVs = strings.Join(keys, ",") 93 | } 94 | } 95 | 96 | func main() { 97 | if C.BVs != "" { 98 | split := strings.Split(C.BVs, ",") 99 | limit := util.NewGoLimit(C.J) 100 | //wg := &sync.WaitGroup{} 101 | for _, v := range split { 102 | limit.Add() 103 | //wg.Add(1) 104 | v := v 105 | go func() { 106 | defer limit.Done() 107 | //defer wg.Done() 108 | video, err := api.VideoFromBV(v) 109 | if err != nil { 110 | log.Println(err) 111 | return 112 | } 113 | _, err = api.ResolveVideo(video) 114 | if err != nil { 115 | log.Println(err) 116 | return 117 | } 118 | stream, err := api.GetStream(*video) 119 | if err != nil { 120 | log.Println(err) 121 | return 122 | } 123 | err = api.Dl(stream) 124 | if err != nil { 125 | log.Println(err) 126 | return 127 | } 128 | if C.FFMPEG { 129 | err := api.Merge(stream) 130 | if err != nil { 131 | log.Println(err) 132 | } 133 | } 134 | }() 135 | } 136 | limit.Wait() 137 | } 138 | if C.UP != "" { 139 | videos, err := api.AllVideo(C.UP) 140 | if err != nil { 141 | log.Fatalln(err) 142 | } 143 | limit := util.NewGoLimit(C.J) 144 | for _, v := range videos { 145 | limit.Add() 146 | go func(v api.Video) { 147 | defer limit.Done() 148 | _, err := api.ResolveVideo(&v) 149 | if err != nil { 150 | log.Println(err) 151 | return 152 | } 153 | for i := 0; i < 3; i++ { 154 | stream, err := api.GetStream(v) 155 | if err != nil { 156 | log.Println(err) 157 | continue 158 | } 159 | err = api.Dl(stream) 160 | if err != nil { 161 | log.Println(err) 162 | continue 163 | } 164 | if C.FFMPEG { 165 | if C.Merge { 166 | err := api.Merge(stream) 167 | if err != nil { 168 | log.Println(err) 169 | } 170 | } 171 | } 172 | break 173 | } 174 | }(v) 175 | } 176 | limit.Wait() 177 | } 178 | log.Println("下载完成") 179 | } 180 | -------------------------------------------------------------------------------- /util/limit.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "sync" 4 | 5 | type GoLimit struct { 6 | c chan struct{} 7 | wait *sync.WaitGroup 8 | } 9 | 10 | func NewGoLimit(max int) *GoLimit { 11 | return &GoLimit{ 12 | c: make(chan struct{}, max), 13 | wait: &sync.WaitGroup{}, 14 | } 15 | } 16 | 17 | func (g *GoLimit) Add() { 18 | g.c <- struct{}{} 19 | g.wait.Add(1) 20 | } 21 | 22 | func (g *GoLimit) Done() { 23 | <-g.c 24 | g.wait.Done() 25 | } 26 | 27 | func (g *GoLimit) Wait() { 28 | g.wait.Wait() 29 | } 30 | --------------------------------------------------------------------------------