aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/biliapi.go94
-rw-r--r--internal/download/downloader.go141
2 files changed, 235 insertions, 0 deletions
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
+}