├── 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 |
--------------------------------------------------------------------------------