aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryingyu5658 <i@yingyu5658.me>2025-12-04 11:19:57 +0800
committeryingyu5658 <i@yingyu5658.me>2025-12-04 11:19:57 +0800
commitc52f41545df179b542f106d08e6dd013a70216e7 (patch)
treeda54d63c0ad311f77fd1ff7d3b3f53109ed99af7
parent14a941b09749d63871016831b657f6de7a6337fc (diff)
downloadbvd-c52f41545df179b542f106d08e6dd013a70216e7.tar.gz
bvd-c52f41545df179b542f106d08e6dd013a70216e7.zip
refactor: 模块化重构并优化输出
- 将原本的 main.go 拆分为 命令 网络请求 下载三个单独职责的文件 - 优化输出, 添加用户友好的Emoji
-rw-r--r--cmd/bvd/main.go42
-rw-r--r--internal/api/biliapi.go94
-rw-r--r--internal/download/downloader.go141
-rw-r--r--main.go221
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
-}