diff options
| -rw-r--r-- | cmd/bvd/main.go | 42 | ||||
| -rw-r--r-- | internal/api/biliapi.go | 94 | ||||
| -rw-r--r-- | internal/download/downloader.go | 141 | ||||
| -rw-r--r-- | main.go | 221 |
4 files changed, 277 insertions, 221 deletions
diff --git a/cmd/bvd/main.go b/cmd/bvd/main.go new file mode 100644 index 0000000..3a537b4 --- /dev/null +++ b/cmd/bvd/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "os" + + "bvd/internal/api" + downloader "bvd/internal/download" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "bvd", + Usage: "快速、高效、易用的下载B站视频 CLI 工具", + Commands: []*cli.Command{ + { + Name: "download", + Usage: "下载指定 BV 号的视频", + Action: func(c *cli.Context) error { + bvid := c.Args().First() + if bvid == "" { + return fmt.Errorf("请提供视频 BV 号") + } + + biliAPI := api.NewBiliAPI() + downloader := downloader.NewDownloader() + + return downloader.Start(bvid, biliAPI) + }, + ArgsUsage: "<BVID> 欲下载视频的 BV 号", + }, + }, + Version: "1.0.1", + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/internal/api/biliapi.go b/internal/api/biliapi.go new file mode 100644 index 0000000..efd2499 --- /dev/null +++ b/internal/api/biliapi.go @@ -0,0 +1,94 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +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"` +} + +type BiliAPI struct { + Client *http.Client +} + +func NewBiliAPI() *BiliAPI { + return &BiliAPI{ + Client: &http.Client{Timeout: 30 * time.Second}, + } +} + +// func getInfo(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 VideoBaseInfo{}, err +// } + +func (a *BiliAPI) GetVideoInfo(bvid string) (*VideoBaseInfo, error) { + var APIUrl string = "https://api.bilibili.com/x/player/pagelist?bvid=" + APIUrl += bvid + client := a.Client + if client == nil { + 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) + } + + if len(body) > 0 && body[0] == '<' { + snippet := string(body) + if len(snippet) > 512 { + snippet = snippet[:512] + } + return nil, fmt.Errorf("❌ 服务器返回 HTML 而非 JSON: %s", snippet) + } + + var result VideoBaseInfo + if err := json.Unmarshal(body, &result); err != nil { + return &VideoBaseInfo{}, fmt.Errorf("❌ JSON 解析失败: %w", err) + } + + return &result, nil + +} diff --git a/internal/download/downloader.go b/internal/download/downloader.go new file mode 100644 index 0000000..24a91db --- /dev/null +++ b/internal/download/downloader.go @@ -0,0 +1,141 @@ +package downloader + +import ( + "bvd/internal/api" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +type Downloader struct{} + +func NewDownloader() *Downloader { + return &Downloader{} +} + +func requestSend(urls []string, video *api.VideoBaseInfo) error { + downloadDir := "./downloads/" + if err := os.MkdirAll(downloadDir, 0755); err != nil { + return fmt.Errorf("❌ 创建下载目录失败:%w", err) + } + + 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 +} + +func (d *Downloader) Start(bvid string, apiClient *api.BiliAPI) error { + fmt.Println("✅ 获取到 BV 号:", bvid) + + videoInfo, err := apiClient.GetVideoInfo(bvid) + if err != nil { + return err + } + + // 2. 提取 提取所有 CID + cids := make([]int, len(videoInfo.Data)) + for i, page := range videoInfo.Data { + cids[i] = page.Cid + } + + fmt.Println("✅ 获取到 CID 列表", cids) + + // 3. 为每个CID获取下载链接 + downloadUrls := make([]string, len(cids)) + + for j, cid := range cids { + apiUrl := fmt.Sprintf("https://api.bilibili.com/x/player/playurl?&cid=%d&bvid=%s&qn=80", cid, bvid) + + resp, err := apiClient.Client.Get(apiUrl) + if err != nil { + return fmt.Errorf("❌ CID %d 请求 请求失败: %w", cid, err) + } + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("❌ CID %d 读取响应失败: %w", cid, err) + } + + var result struct { + Data struct { + Durl []struct { + Url string `json:"url"` + } `json:"durl"` + } `json:"data"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return fmt.Errorf("❌ CID %d JSON解析失败: %w", cid, err) + } + + if len(result.Data.Durl) == 0 { + return fmt.Errorf("❌ CID %d 没有 没有获取到下载链接", cid) + } + + downloadUrls[j] = result.Data.Durl[0].Url + } + + fmt.Println("✅ 获取到所有下载链接") + + requestSend(downloadUrls, videoInfo) + + return nil +} diff --git a/main.go b/main.go deleted file mode 100644 index 4c296f3..0000000 --- a/main.go +++ /dev/null @@ -1,221 +0,0 @@ -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 -} |
