aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--go.mod10
-rw-r--r--go.sum8
-rw-r--r--main.go230
4 files changed, 249 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..5f33181
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module bvd
+
+go 1.25.0
+
+require (
+ github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/urfave/cli/v2 v2.27.7 // indirect
+ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c851d15
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,8 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
+github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
+github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
+github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
+github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..0fae7f4
--- /dev/null
+++ b/main.go
@@ -0,0 +1,230 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/urfave/cli/v2"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+const AppVersion = "1.0.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
+ }
+
+ fmt.Printf("成功获取 %d 个下载链接(每个CID一个)\n", len(downloadUrls))
+ fmt.Printf("下载链接:%s\n", downloadUrls)
+ fmt.Printf("下载链接1:%s\n", downloadUrls[0])
+ fmt.Printf("下载链接2:%s\n", downloadUrls[1])
+ return downloadUrls, nil
+}
+
+func downloadVideo(video VideoBaseInfo, bvid string) error {
+ fmt.Println("downloadVideo is running")
+ 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.Println("url[1]", urls[1])
+ fmt.Printf("开始下载第 %d 个文件\n", i+1)
+ fmt.Printf("Url: %s\n", urls[i])
+
+ // 创建支持重定向的 HTTP 客户端
+ client := &http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ fmt.Printf("重定向到: %s\n", req.URL)
+ 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
+}