1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
package main
import (
"encoding/json"
"fmt"
"github.com/urfave/cli/v2"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
)
const AppVersion = "0.1.0"
type VideoBaseInfo struct {
Data []struct {
Cid int `json:"cid"` // 每一个视频的 CID
Part string `json:"part"` // 分 P 标题
Page int `json:"page"` // 分 P 编号
FirstFrame string `json:"first_frame"` // 封面图
} `json:"data"`
}
// getDownloadUrl函数得到的JSON的匹配结构体
type getDownloadUrlJson struct {
Data struct {
Durl []struct {
Url string `json: "durl"`
} `json:"durl"`
} `json: "data"`
}
func main() {
app := &cli.App{
Name: "bvd",
Usage: "快速、高效、易用的下载B站视频 CLI 工具",
Commands: []*cli.Command{
{
Name: "download",
Usage: "下载指定 BV 号的视频",
Action: downloadAction,
ArgsUsage: "<BVID> 欲下载视频的 BV 号",
},
},
Action: func(c *cli.Context) error {
args := c.Args()
if args.Len() == 0 {
cli.ShowAppHelp(c)
}
return nil
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func downloadAction(c *cli.Context) error {
// 从命令行获取 BVID 参数
bvid := c.Args().First()
if bvid == "" {
return fmt.Errorf("请提供 BV 号")
}
return startDownload(bvid)
}
func startDownload(bvid string) error {
video, err := getVideoBaseInfo(bvid)
if err != nil {
return fmt.Errorf("获取视频信息失败:%w", err)
}
err = downloadVideo(video, bvid)
if err != nil {
return err
}
return nil
}
func getVideoBaseInfo(bvid string) (VideoBaseInfo, error) {
var APIUrl string = "https://api.bilibili.com/x/player/pagelist?bvid="
APIUrl += bvid
client := &http.Client{
Timeout: 30 * time.Second,
}
// 发送 GET 请求
resp, err := client.Get(APIUrl)
if err != nil {
return VideoBaseInfo{}, fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return VideoBaseInfo{}, fmt.Errorf("读取响应失败: %w", err)
}
var result VideoBaseInfo
if err := json.Unmarshal(body, &result); err != nil {
return VideoBaseInfo{}, fmt.Errorf("JSON 解析失败: %w", err)
}
return result, nil
}
func getDownloadUrl(video VideoBaseInfo, bvid string) ([]string, error) {
cid := make([]int, len(video.Data))
for i := 0; i < len(video.Data); i++ {
cid[i] = video.Data[i].Cid
}
var downloadUrls = make([]string, len(video.Data))
var result getDownloadUrlJson
client := &http.Client{Timeout: 30 * time.Second}
for j := 0; j < len(cid); j++ {
apiUrl := fmt.Sprintf("https://api.bilibili.com/x/player/playurl?&cid=%d&bvid=%s&qn=80", cid[j], bvid)
resp, err := client.Get(apiUrl)
if err != nil {
return nil, fmt.Errorf("CID %d 请求失败: %w", cid[j], err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("CID %d 读取响应失败: %w", cid[j], err)
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("CID %d JSON解析失败: %w", cid[j], err)
}
downloadUrls[j] = result.Data.Durl[0].Url
}
return downloadUrls, nil
}
func downloadVideo(video VideoBaseInfo, bvid string) error {
var urls []string
urls, err := getDownloadUrl(video, bvid)
if err != nil {
return fmt.Errorf("获取视频下载链接失败:%w", err)
}
// 创建下载目录
downloadDir := "./downloads/"
if err := os.MkdirAll(downloadDir, 0755); err != nil {
return fmt.Errorf("创建下载目录失败:%w", err)
}
fmt.Printf("成功获取 %d 个下载链接\n", len(urls))
for i := 0; i < len(urls); i++ {
fmt.Printf("开始下载第 %d 个文件\n", i+1)
// 创建支持重定向的 HTTP 客户端
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return nil
},
}
req, err := http.NewRequest("GET", urls[i], nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("referer", "https://www.bilibili.com")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("HTTP请求失败:%w", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
}
// 构建文件名
filename := filepath.Join(downloadDir, video.Data[i].Part+".mp4")
fmt.Printf("保存到: %s\n", filename)
// 创建文件
out, err := os.Create(filename)
if err != nil {
return fmt.Errorf("创建文件失败:%w", err)
}
// 下载文件
_, err = io.Copy(out, resp.Body)
if err != nil {
out.Close()
return fmt.Errorf("下载失败:%w", err)
}
// 关闭文件
if err := out.Close(); err != nil {
return fmt.Errorf("关闭文件失败:%w", err)
}
fmt.Printf("文件下载成功: %s\n", filename)
}
return nil
}
|