diff --git a/.gitignore b/.gitignore index 4c0a5e5..df64652 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ package-lock.json # 程序生成 config.json Cache/* -Download/* \ No newline at end of file +Download/* + +.agent \ No newline at end of file diff --git a/actions.go b/actions.go deleted file mode 100644 index 8963a87..0000000 --- a/actions.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "bili-audio-downloader/backend/config" - wails "github.com/wailsapp/wails/v2/pkg/runtime" -) - -// OpenFileDialog 调用打开文件窗口 -func (a *App) OpenFileDialog() (string, error) { - var FileFilter []wails.FileFilter - - fileFilter := wails.FileFilter{ - DisplayName: "视频下载列表 (*.json)", - Pattern: "*.json", - } - FileFilter = append(FileFilter, fileFilter) - - option := wails.OpenDialogOptions{ - DefaultDirectory: "./", - DefaultFilename: "", - Title: "打开本地列表文件", - Filters: FileFilter, - } - // 弹出对话框 - path, err := wails.OpenFileDialog(a.ctx, option) - if err != nil { - wails.LogErrorf(a.ctx, err.Error()) - return "", err - } - - return path, nil -} - -func (a *App) SetDownloadPathDialog() { - - option := wails.OpenDialogOptions{ - DefaultDirectory: "./", - DefaultFilename: "", - Title: "选择下载路径", - } - - path, err := wails.OpenDirectoryDialog(a.ctx, option) - if err != nil { - wails.EventsEmit(a.ctx, "error", "错误:"+err.Error()) - } - - config.Cfg.FileConfig.DownloadPath = path - err = config.Cfg.UpdateAndSave() - if err != nil { - wails.EventsEmit(a.ctx, "error", "错误:"+err.Error()) - } - -} - -//// 调用保存窗口 -//func (a *App) SaveVideoListTo(videolist services.VideoList) error { -// var FileFilter []wails.FileFilter -// -// fileFilter := wails.FileFilter{ -// DisplayName: "视频下载列表 (*.json)", -// Pattern: "*.json", -// } -// FileFilter = append(FileFilter, fileFilter) -// -// option := wails.SaveDialogOptions{ -// DefaultDirectory: "./", -// DefaultFilename: "BAD_VideoList", -// Title: "另存视频列表", -// Filters: FileFilter, -// } -// -// // 弹出对话框 -// path, err := wails.SaveFileDialog(a.ctx, option) -// if err != nil { -// return err -// } -// -// // 用户取消操作 -// if path == "" { -// wails.EventsEmit(a.ctx, "error", "未选择保存路径") -// return nil -// } -// -// // 保存列表 -// err = videolist.Save(path) -// if err != nil { -// return err -// } -// return nil -//} - -// 打开下载文件夹 -// TODO -func (a *App) OpenDownloadFolader() error { - - //err := OpenFolder(config.Cfg.GetDownloadPath()) - //if err != nil { - // return err - //} - - return nil -} diff --git a/backend/adapter/adapter.go b/backend/adapter/adapter.go index 5e8460e..ecbb657 100644 --- a/backend/adapter/adapter.go +++ b/backend/adapter/adapter.go @@ -29,4 +29,5 @@ type TaskInfo struct { SongName string SongAuthor string CoverUrl string + IsDelete bool } diff --git a/backend/adapter/bilibili/audio.go b/backend/adapter/bilibili/audio.go index 87e9a17..b1f9a44 100644 --- a/backend/adapter/bilibili/audio.go +++ b/backend/adapter/bilibili/audio.go @@ -20,6 +20,7 @@ type Audio struct { option adapter.Option path adapter.Path metaData adapter.MetaData + isDelete bool } func (a *Audio) SetID(id int) { @@ -38,10 +39,23 @@ func NewAudio(auid string, coverUrl, sessdata string, metaData adapter.MetaData) OutputFormat: constants.AudioType.M4a, }, metaData: metaData, + isDelete: false, } } +func (a *Audio) SetDelete(del bool) { + a.isDelete = del +} + +func (a *Audio) SetMeta(songName, author string) { + a.metaData.SongName = songName + a.metaData.Author = author +} + func (a *Audio) Download() error { + if a.isDelete { + return nil + } var wg sync.WaitGroup errorResults := make(chan error, 2) @@ -77,7 +91,7 @@ func (a *Audio) Download() error { var err error for i := 0; i < config.Cfg.DownloadConfig.RetryCount; i++ { - err = utils.SaveFromURL(a.coverUrl, a.path.CoverPath) + err = utils.SaveFromURL(a.coverUrl, a.path.CoverPath, bilibili.GetRandomUA()) if err != nil { err = errors.New(fmt.Sprintf("failed to download video stream: %v (retry %d)", err, i)) continue @@ -116,7 +130,7 @@ func (a *Audio) ConventFormat() error { } func (a *Audio) WriteMetadata() error { - newPath := fmt.Sprintf("%s.meta", a.path.StreamPath) + newPath := fmt.Sprintf("%s_meta%s", a.path.StreamPath, a.path.OutputFormat) err := ffmpeg.WriteMetadata(a.path.CurrentPath, newPath, a.path.CoverPath, a.metaData.SongName, a.metaData.Author, a.path.OutputFormat) if err != nil { return err @@ -143,7 +157,7 @@ func (a *Audio) downloadStream(streamPath string) error { } // 通过流地址下载 - err = utils.StreamingDownloader(audio.Stream.StreamLink, streamPath) + err = utils.StreamingDownloader(audio.Stream.StreamLink, streamPath, bilibili.GetRandomUA()) if err != nil { return errors.New(fmt.Sprintf("failed to download stream: %s", err)) } @@ -155,6 +169,7 @@ func (a *Audio) GetTaskInfo() *adapter.TaskInfo { SongName: a.metaData.SongName, SongAuthor: a.metaData.Author, CoverUrl: a.coverUrl, + IsDelete: a.isDelete, } return &taskInfo } diff --git a/backend/adapter/bilibili/video.go b/backend/adapter/bilibili/video.go index 93bfebe..e32c38f 100644 --- a/backend/adapter/bilibili/video.go +++ b/backend/adapter/bilibili/video.go @@ -9,9 +9,10 @@ import ( "bili-audio-downloader/bilibili" "errors" "fmt" - "github.com/tidwall/gjson" "strconv" "sync" + + "github.com/tidwall/gjson" ) type Video struct { @@ -23,6 +24,7 @@ type Video struct { option adapter.Option path adapter.Path metaData adapter.MetaData + isDelete bool } func NewVideo(bvid string, cid int, coverUrl, sessdata string, metaData adapter.MetaData) *Video { @@ -38,6 +40,7 @@ func NewVideo(bvid string, cid int, coverUrl, sessdata string, metaData adapter. OutputFormat: constants.AudioType.M4a, }, metaData: metaData, + isDelete: false, } } @@ -45,7 +48,20 @@ func (v *Video) SetID(id int) { v.listId = id } +func (v *Video) SetDelete(del bool) { + v.isDelete = del +} + +func (v *Video) SetMeta(songName, author string) { + v.metaData.SongName = songName + v.metaData.Author = author +} + func (v *Video) Download() error { + if v.isDelete { + return nil + } + var wg sync.WaitGroup errorResults := make(chan error, 2) @@ -80,7 +96,7 @@ func (v *Video) Download() error { var err error for i := 0; i < config.Cfg.DownloadConfig.RetryCount; i++ { - err = utils.SaveFromURL(v.coverUrl, v.path.CoverPath) + err = utils.SaveFromURL(v.coverUrl, v.path.CoverPath, bilibili.GetRandomUA()) if err != nil { err = errors.New(fmt.Sprintf("failed to download video stream: %v (retry %d)", err, i)) continue @@ -129,7 +145,7 @@ func (v *Video) downloadStream(streamPath string) error { } // 通过流地址下载 - err = utils.StreamingDownloader(stream, streamPath) + err = utils.StreamingDownloader(stream, streamPath, bilibili.GetRandomUA()) if err != nil { return errors.New(fmt.Sprintf("failed to download stream: %s", err)) } @@ -179,6 +195,7 @@ func (w *Video) GetTaskInfo() *adapter.TaskInfo { SongName: w.metaData.SongName, SongAuthor: w.metaData.Author, CoverUrl: w.coverUrl, + IsDelete: w.isDelete, } return &taskInfo } diff --git a/backend/config/config.go b/backend/config/config.go index 3c7a684..f773189 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -5,11 +5,12 @@ import ( "context" "errors" "fmt" - wails "github.com/wailsapp/wails/v2/pkg/runtime" "log" "os" "path/filepath" + wails "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/spf13/viper" ) @@ -165,14 +166,14 @@ func DefaultConfig() *Config { cfg := Config{ ConfigVersion: constants.CONFIG_VERSION, DeleteCache: true, - Theme: "lightPink", + Theme: "lightBlue", DownloadConfig: DownloadConfig{ DownloadThreads: 5, RetryCount: 10, }, FileConfig: FileConfig{ ConvertFormat: false, // TODO - FileNameTemplate: "{{.ID}}_{{.Title}}({{.Subtitle}})_{{.Quality}}.{{.Format}}", + FileNameTemplate: "{{.ID}}_{{.Title}}({{.Subtitle}})_{{.Quality}}{{.Format}}", DownloadPath: "./Download", CachePath: "./Cache", VideoListPath: "./Cache/video_list.json", diff --git a/backend/constants/constants.go b/backend/constants/constants.go index e71f15c..daaa612 100644 --- a/backend/constants/constants.go +++ b/backend/constants/constants.go @@ -1,7 +1,7 @@ package constants const CONFIG_VERSION int = 2 -const APP_VERSION string = "4.10.0" +const APP_VERSION string = "4.10.1-rc1" var AudioType = struct { M4a string diff --git a/backend/download/download.go b/backend/download/download.go index fea9e85..83c717e 100644 --- a/backend/download/download.go +++ b/backend/download/download.go @@ -11,4 +11,6 @@ type DownloadTask interface { WriteMetadata() error ExportFile() error GetTaskInfo() *adapter.TaskInfo + SetDelete(bool) + SetMeta(string, string) } diff --git a/backend/download/task_manager.go b/backend/download/task_manager.go index 807038e..44043b7 100644 --- a/backend/download/task_manager.go +++ b/backend/download/task_manager.go @@ -5,8 +5,9 @@ import ( bilibili2 "bili-audio-downloader/backend/adapter/bilibili" "bili-audio-downloader/backend/utils" "bili-audio-downloader/bilibili" - "github.com/tidwall/gjson" "strconv" + + "github.com/tidwall/gjson" ) var DownloadList []DownloadTask @@ -119,6 +120,12 @@ func AddCollectionTask(sessdata, favlistId string, count int, downloadCompilatio // 主循环 for i := 0; i < pageCount; i++ { + pageSize := 20 + // 处理非完整尾页 + if i+1 == pageCount && count%20 != 0 { + pageSize = count % 20 + } + // 获取当前分页信息 favlist, err := bilibili.GetFavListObj(favlistId, sessdata, 20, i+1) if err != nil { @@ -126,7 +133,7 @@ func AddCollectionTask(sessdata, favlistId string, count int, downloadCompilatio } // 遍历分页 - for j := 0; j < len(favlist.Data.Medias); j++ { + for j := 0; j < len(favlist.Data.Medias) && j < pageSize; j++ { if favlist.Data.Medias[j].Type == 2 { // 添加视频到列表 @@ -170,13 +177,19 @@ func AddCompilationTask(sessdata string, mid, sid, count int, downloadCompilatio // 主循环 for i := 0; i < pageCount; i++ { + pageSize := 20 + // 处理非完整尾页 + if i+1 == pageCount && count%20 != 0 { + pageSize = count % 20 + } + // 获取当前分页信息 favlist, err := bilibili.GetCompliationObj(mid, sid, 20, i+1) if err != nil { return err } // 遍历分页 - for j := 0; j < len(favlist.Data.Archives); j++ { + for j := 0; j < len(favlist.Data.Archives) && j < pageSize; j++ { // 添加视频到列表 err := AddVideoTask(sessdata, favlist.Data.Archives[j].Bvid, downloadCompilation) if err != nil { diff --git a/backend/ffmpeg/metadata_writer.go b/backend/ffmpeg/metadata_writer.go index 97ed20f..7d0bbb9 100644 --- a/backend/ffmpeg/metadata_writer.go +++ b/backend/ffmpeg/metadata_writer.go @@ -62,7 +62,13 @@ func WriteMetadata(input, output, coverPath, songName, songAuthor string, format args = append(args, "-id3v2_version", "3") } - args = append(args, "-f", format[1:], output) + // 确定 ffmpeg 的 -f 参数 + formatFlag := format[1:] + if format == constants.AudioType.M4a { + formatFlag = "mp4" + } + + args = append(args, "-f", formatFlag, output) log, err := utils.RunCommand("ffmpeg", args...) if err != nil { diff --git a/backend/utils/download.go b/backend/utils/download.go index ac43012..8254e45 100644 --- a/backend/utils/download.go +++ b/backend/utils/download.go @@ -13,7 +13,7 @@ import ( // StreamingDownloader 用于下载音频流的函数 // 传入流 URL 和文件名 -func StreamingDownloader(audioURL, filePathAndName string) error { +func StreamingDownloader(audioURL, filePathAndName, ua string) error { // 先判断文件是否存在,如果存在则跳过下载,否则创建文件 out, err := os.Create(filePathAndName) if err != nil { @@ -28,6 +28,7 @@ func StreamingDownloader(audioURL, filePathAndName string) error { return err } request.Header.Set("referer", "https://www.bilibili.com") + request.Header.Set("User-Agent", ua) response, err := client.Do(request) if err != nil { return err @@ -42,7 +43,7 @@ func StreamingDownloader(audioURL, filePathAndName string) error { } // 从 URL 下载图片 -func SaveFromURL(url string, filePath string) error { +func SaveFromURL(url string, filePath, ua string) error { file, err := os.Create(filePath) if err != nil { return err @@ -50,7 +51,14 @@ func SaveFromURL(url string, filePath string) error { defer file.Close() // 发起 HTTP 请求获取图片内容 - response, err := http.Get(url) + client := &http.Client{} + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + request.Header.Set("User-Agent", ua) + + response, err := client.Do(request) if err != nil { return err } diff --git a/backend/utils/utils.go b/backend/utils/utils.go index 53b1533..dad9583 100644 --- a/backend/utils/utils.go +++ b/backend/utils/utils.go @@ -89,3 +89,10 @@ func RunCommand(name string, args ...string) (string, error) { return out.String(), nil } + +// 打开文件夹 +func OpenFolder(path string) error { + cmd := exec.Command("cmd", "/c", "start", "", path) + // services.setHideWindow(cmd) + return cmd.Start() +} diff --git a/backend/wails_api/actions.go b/backend/wails_api/actions.go new file mode 100644 index 0000000..570de29 --- /dev/null +++ b/backend/wails_api/actions.go @@ -0,0 +1,67 @@ +package wails_api + +import ( + "bili-audio-downloader/backend/config" + "bili-audio-downloader/backend/utils" + + wails "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// OpenFileDialog 调用打开文件窗口 +func (w *WailsApi) OpenFileDialog() (string, error) { + var FileFilter []wails.FileFilter + + fileFilter := wails.FileFilter{ + DisplayName: "视频下载列表 (*.json)", + Pattern: "*.json", + } + FileFilter = append(FileFilter, fileFilter) + + option := wails.OpenDialogOptions{ + DefaultDirectory: "./", + DefaultFilename: "", + Title: "打开本地列表文件", + Filters: FileFilter, + } + // 弹出对话框 + path, err := wails.OpenFileDialog(w.ctx, option) + if err != nil { + wails.LogErrorf(w.ctx, "OpenFileDialog error: %s", err.Error()) + return "", err + } + + return path, nil +} + +func (w *WailsApi) SetDownloadPathDialog() { + + option := wails.OpenDialogOptions{ + DefaultDirectory: "./", + DefaultFilename: "", + Title: "选择下载路径", + } + + path, err := wails.OpenDirectoryDialog(w.ctx, option) + if err != nil { + wails.EventsEmit(w.ctx, "error", "错误:"+err.Error()) + } + + if path != "" { + config.Cfg.FileConfig.DownloadPath = path + err = config.Cfg.UpdateAndSave() + if err != nil { + wails.EventsEmit(w.ctx, "error", "错误:"+err.Error()) + } + } +} + +// 打开下载文件夹 +func (w *WailsApi) OpenDownloadFolader() error { + + err := utils.OpenFolder(config.Cfg.GetDownloadPath()) + if err != nil { + return err + } + + return nil +} diff --git a/logic.go b/backend/wails_api/auth.go similarity index 66% rename from logic.go rename to backend/wails_api/auth.go index 8571847..62f8c91 100644 --- a/logic.go +++ b/backend/wails_api/auth.go @@ -1,4 +1,4 @@ -package main +package wails_api import ( "bili-audio-downloader/backend/config" @@ -11,8 +11,8 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" ) -// 登录 bilibili -func (a *App) LoginBilibili() error { +// LoginBilibili 登录 bilibili +func (w *WailsApi) LoginBilibili() error { // 获取二维码和请求密钥 url, key, err := bilibili.GetLoginKey() if err != nil { @@ -30,7 +30,7 @@ func (a *App) LoginBilibili() error { if err != nil { return err } - runtime.EventsEmit(a.ctx, "qrcodeStr", base64Data) + runtime.EventsEmit(w.ctx, "qrcodeStr", base64Data) // 请求登录 cookies, err := func() (*[]*http.Cookie, error) { @@ -44,22 +44,22 @@ func (a *App) LoginBilibili() error { switch returnObj.Data.Code { case 0: // 登录成功 - runtime.LogDebug(a.ctx, "登录成功") - runtime.EventsEmit(a.ctx, "loginStatus", "登录成功") + runtime.LogDebug(w.ctx, "登录成功") + runtime.EventsEmit(w.ctx, "loginStatus", "登录成功") return cookies, nil case 86038: // 二维码失效 - runtime.LogDebug(a.ctx, "二维码已失效") - runtime.EventsEmit(a.ctx, "loginStatus", "二维码已失效") + runtime.LogDebug(w.ctx, "二维码已失效") + runtime.EventsEmit(w.ctx, "loginStatus", "二维码已失效") return nil, errors.New("二维码已失效") case 86090: // 扫描成功,待确认 - runtime.LogDebug(a.ctx, "扫描成功,待确认") - runtime.EventsEmit(a.ctx, "loginStatus", "扫描成功,待确认") + runtime.LogDebug(w.ctx, "扫描成功,待确认") + runtime.EventsEmit(w.ctx, "loginStatus", "扫描成功,待确认") case 86101: // 未扫描 - runtime.LogDebug(a.ctx, "未扫描") - runtime.EventsEmit(a.ctx, "loginStatus", "请扫描二维码登录") + runtime.LogDebug(w.ctx, "未扫描") + runtime.EventsEmit(w.ctx, "loginStatus", "请扫描二维码登录") } } }() @@ -82,11 +82,3 @@ func (a *App) LoginBilibili() error { return nil } - -// TODO -//// 打开文件夹 -//func OpenFolder(path string) error { -// cmd := exec.Command("cmd", "/c", "start", "", path) -// services.setHideWindow(cmd) -// return cmd.Start() -//} diff --git a/backend/wails_api/config.go b/backend/wails_api/config.go index 8656d4f..b81b8a8 100644 --- a/backend/wails_api/config.go +++ b/backend/wails_api/config.go @@ -2,6 +2,7 @@ package wails_api import ( "bili-audio-downloader/backend/config" + wails "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -12,7 +13,6 @@ func (w *WailsApi) ResetConfig() { if err != nil { wails.LogErrorf(w.ctx, "写入设置文件失败:%s", err) wails.EventsEmit(w.ctx, "error", "写入设置时出错:"+err.Error()) - // TODO 增加统一前端错误提示接口 } } diff --git a/backend/wails_api/videolist.go b/backend/wails_api/videolist.go index 32cd4e3..3e04491 100644 --- a/backend/wails_api/videolist.go +++ b/backend/wails_api/videolist.go @@ -4,6 +4,10 @@ import ( "bili-audio-downloader/backend/adapter" "bili-audio-downloader/backend/config" "bili-audio-downloader/backend/download" + "encoding/json" + "os" + + "github.com/wailsapp/wails/v2/pkg/runtime" ) // GetListCount 获取列表中视频数量 @@ -33,12 +37,17 @@ func (w *WailsApi) GetTaskListAll() []adapter.TaskInfo { func (w *WailsApi) GetTaskListPage(page int) []adapter.TaskInfo { var taskList []adapter.TaskInfo - const PageSize = 10 + const PageSize = 20 + length := len(download.DownloadList) start := page * PageSize - end := page*PageSize + PageSize + end := start + PageSize + + if start >= length { + return []adapter.TaskInfo{} + } - if end > len(download.DownloadList) { - end = len(download.DownloadList) + if end > length { + end = length } for i, task := range download.DownloadList[start:end] { @@ -124,3 +133,60 @@ func (w *WailsApi) AddProfileVideoToList(listPath string, mid, count int, downlo return nil } + +// SetTaskDeleteState 设置任务删除状态 +func (w *WailsApi) SetTaskDeleteState(index int, delete bool) { + if index >= 0 && index < len(download.DownloadList) { + download.DownloadList[index].SetDelete(delete) + } +} + +// UpdateTaskMeta 更新任务元数据 +func (w *WailsApi) UpdateTaskMeta(index int, songName, author string) { + if index >= 0 && index < len(download.DownloadList) { + download.DownloadList[index].SetMeta(songName, author) + } +} + +// ExportVideoList 导出任务列表(带对话框) +func (w *WailsApi) ExportVideoList() error { + path, err := runtime.SaveFileDialog(w.ctx, runtime.SaveDialogOptions{ + Title: "保存列表", + DefaultFilename: "video_list.json", + Filters: []runtime.FileFilter{ + {DisplayName: "JSON Files (*.json)", Pattern: "*.json"}, + }, + }) + if err != nil { + return err + } + if path == "" { + return nil + } + return w.SaveVideoListTo(path) +} + +// SaveVideoListTo 导出任务列表 +func (w *WailsApi) SaveVideoListTo(path string) error { + var listToExport []adapter.TaskInfo + for i, task := range download.DownloadList { + info := task.GetTaskInfo() + info.Index = i + listToExport = append(listToExport, *info) + } + + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(listToExport) + if err != nil { + return err + } + + return nil +} diff --git a/bilibili/Audio.go b/bilibili/Audio.go index 7690d93..1b20cd6 100644 --- a/bilibili/Audio.go +++ b/bilibili/Audio.go @@ -1,15 +1,5 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "net/url" - "strconv" - - "github.com/tidwall/gjson" -) - // // 用于获取 AUID 音频流信息 // type audio struct { // Code int `json:"code"` @@ -53,81 +43,3 @@ type Audio struct { StreamLink string `json:"stream_link"` // 音频流列表 } } - -func (audio *Audio) Query(auid string) error { - - // 设置 URL 并发送 GET 请求 - params := url.Values{} - Url, _ := url.Parse("https://www.bilibili.com/audio/music-service-c/web/song/info") - - // 设置 URL 参数 - params.Set("sid", auid) - - Url.RawQuery = params.Encode() - urlPath := Url.String() - resp, err := http.Get(urlPath) - if err != nil { - return err - } - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyJson := string(body) - defer resp.Body.Close() - - audio.Auid = auid - audio.Meta.Title = gjson.Get(bodyJson, "data.title").String() - audio.Meta.Cover = gjson.Get(bodyJson, "data.cover").String() - audio.Meta.Lyric = gjson.Get(bodyJson, "data.lyric").String() - audio.Up.Author = gjson.Get(bodyJson, "data.author").String() - - return nil -} - -func (audio *Audio) GetStream(sessdata string) error { - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/audio/music-service-c/url", nil) - if err != nil { - return err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("songid", audio.Auid) - q.Add("quality", "2") - q.Add("privilege", "2") - q.Add("mid", "2") - q.Add("platform", "web") - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyJson := string(body) - - // 错误检查 - if CheckObj(int(gjson.Get(bodyJson, "code").Int())) { - return errors.New(gjson.Get(bodyJson, "message").String()) - } - - audio.Stream.Type = int(gjson.Get(bodyJson, "data.type").Int()) - audio.Stream.StreamLink = gjson.Get(bodyJson, "data.cdns.0").String() - - return nil -} diff --git a/bilibili/Video.go b/bilibili/Video.go index e0ec679..e52228a 100644 --- a/bilibili/Video.go +++ b/bilibili/Video.go @@ -1,14 +1,5 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "strconv" - - "github.com/tidwall/gjson" -) - type Video struct { Bvid string `json:"bvid"` Meta struct { @@ -31,116 +22,3 @@ type Videos struct { SongName string `json:"song_name"` // 歌名 } } - -// 请求视频详细信息 -// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/info.md -// TODO:重新添加字幕信息 -func (v *Video) Query(sessdata, bvid string) error { - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/view", nil) - if err != nil { - return err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("bvid", bvid) - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - json := string(body) - - // 将信息写入结构体 - v.Bvid = bvid - v.Meta.Title = gjson.Get(json, "data.title").String() // 视频标题 - v.Meta.Cover = gjson.Get(json, "data.pic").String() // 视频封面 - v.Meta.LyricsPath = gjson.Get(json, "data.subtitle.0.subtitle_url").String() // 字幕获取(临时) - v.Up.Mid = int(gjson.Get(json, "data.owner.mid").Int()) // UP MID - v.Up.Name = gjson.Get(json, "data.owner.name").String() // UP 昵称 - v.Up.Avatar = gjson.Get(json, "data.owner.face").String() // UP 头像 - - // 根据分 P 数量写入对应信息 - for i := 0; i < int(gjson.Get(json, "data.videos").Int()); i++ { - - // 单个分集视频信息 - videos := Videos{ - Cid: int(gjson.Get(json, "data.pages."+strconv.Itoa(i)+".cid").Int()), - Part: gjson.Get(json, "data.pages."+strconv.Itoa(i)+".part").String(), - } - v.Videos = append(v.Videos, videos) - } - - return nil -} - -// 获取视频流 -// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md#%E8%8E%B7%E5%8F%96%E8%A7%86%E9%A2%91%E6%B5%81%E5%9C%B0%E5%9D%80_web%E7%AB%AF -func GetVideoStream(bvid, cid, sessdata string) (string, error) { - // 创建请求 - request, err := http.NewRequest("GET", "https://api.bilibili.com/x/player/wbi/playurl", nil) - if err != nil { - return "", err - } - - // 设置 URL 参数 - q := request.URL.Query() - q.Add("bvid", bvid) - q.Add("cid", cid) - q.Add("fnval", "16") - request.URL.RawQuery = q.Encode() - - signedUrl, err := WbiSignURLParams(request.URL.String()) - if err != nil { - return "", errors.New("Wbi Sign Error: " + err.Error()) - } - - signedRequest, err := http.NewRequest("GET", signedUrl, nil) - if err != nil { - return "", errors.New("New Signed Request Error: " + err.Error()) - } - - signedRequest.Header.Set("referer", "https://www.bilibili.com") - signedRequest.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0") - - // 添加 Cookie 到请求头 - if sessdata != "" { - signedRequest.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(signedRequest) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} diff --git a/bilibili/api.go b/bilibili/api.go new file mode 100644 index 0000000..121d7d6 --- /dev/null +++ b/bilibili/api.go @@ -0,0 +1,456 @@ +package bilibili + +import ( + "errors" + "io" + "net/http" + "strconv" + "strings" + + "github.com/tidwall/gjson" +) + +// Audio.go API + +// Query query audio streams +func (audio *Audio) Query(auid string) error { + // 设置 URL 并发送 GET 请求 + urlPath := "https://www.bilibili.com/audio/music-service-c/web/song/info" + + body, err := Request(urlPath, WithParams(map[string]string{"sid": auid})) + if err != nil { + return err + } + bodyJson := string(body) + + audio.Auid = auid + audio.Meta.Title = gjson.Get(bodyJson, "data.title").String() + audio.Meta.Cover = gjson.Get(bodyJson, "data.cover").String() + audio.Meta.Lyric = gjson.Get(bodyJson, "data.lyric").String() + audio.Up.Author = gjson.Get(bodyJson, "data.author").String() + + return nil +} + +// GetStream get audio stream url +func (audio *Audio) GetStream(sessdata string) error { + // 创建请求 + urlStr := "https://api.bilibili.com/audio/music-service-c/url" + + // 设置 URL 参数 + params := map[string]string{ + "songid": audio.Auid, + "quality": "2", + "privilege": "2", + "mid": "2", + "platform": "web", + } + + body, err := Request(urlStr, WithSESSDATA(sessdata), WithParams(params)) + if err != nil { + return err + } + bodyJson := string(body) + + // 错误检查 + if CheckObj(int(gjson.Get(bodyJson, "code").Int())) { + return errors.New(gjson.Get(bodyJson, "message").String()) + } + + audio.Stream.Type = int(gjson.Get(bodyJson, "data.type").Int()) + audio.Stream.StreamLink = gjson.Get(bodyJson, "data.cdns.0").String() + + return nil +} + +// Video.go API + +// 请求视频详细信息 +// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/info.md +// TODO:重新添加字幕信息 +func (v *Video) Query(sessdata, bvid string) error { + // 创建请求 + urlStr := "https://api.bilibili.com/x/web-interface/view" + + body, err := Request(urlStr, WithSESSDATA(sessdata), WithParams(map[string]string{"bvid": bvid})) + if err != nil { + return err + } + + json := string(body) + + // 将信息写入结构体 + v.Bvid = bvid + v.Meta.Title = gjson.Get(json, "data.title").String() // 视频标题 + v.Meta.Cover = gjson.Get(json, "data.pic").String() // 视频封面 + v.Meta.LyricsPath = gjson.Get(json, "data.subtitle.0.subtitle_url").String() // 字幕获取(临时) + v.Up.Mid = int(gjson.Get(json, "data.owner.mid").Int()) // UP MID + v.Up.Name = gjson.Get(json, "data.owner.name").String() // UP 昵称 + v.Up.Avatar = gjson.Get(json, "data.owner.face").String() // UP 头像 + + // 根据分 P 数量写入对应信息 + for i := 0; i < int(gjson.Get(json, "data.videos").Int()); i++ { + + // 单个分集视频信息 + videos := Videos{ + Cid: int(gjson.Get(json, "data.pages."+strconv.Itoa(i)+".cid").Int()), + Part: gjson.Get(json, "data.pages."+strconv.Itoa(i)+".part").String(), + } + v.Videos = append(v.Videos, videos) + } + + return nil +} + +// 获取视频流 +// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md#%E8%8E%B7%E5%8F%96%E8%A7%86%E9%A2%91%E6%B5%81%E5%9C%B0%E5%9D%80_web%E7%AB%AF +func GetVideoStream(bvid, cid, sessdata string) (string, error) { + // 创建请求 + urlStr := "https://api.bilibili.com/x/player/wbi/playurl" + + // 设置 URL 参数 + params := map[string]string{ + "bvid": bvid, + "cid": cid, + "fnval": "16", + } + + // This particular API uses WbiSignURLParams which was called manually in the original code. + // Since we use the Wbi option, it should be handled by requests.go logic if we use WithWbi(). + // However, Request() with WithWbi() will sign the params. + // Original code: + // signedUrl, err := WbiSignURLParams(request.URL.String()) + // Then creates new request with signedUrl. + + body, err := Request(urlStr, WithParams(params), WithWbi(), WithSESSDATA(sessdata)) + if err != nil { + return "", err + } + return string(body), nil +} + +// login.go API + +// 获取登录密钥 +func GetLoginKey() (string, string, error) { + var obj GetLoginKeyReturn + body, err := getLoginKey() + if err != nil { + return "", "", err + } + err = decodeJson(body, &obj) + if err != nil { + return "", "", err + } + // 错误检查 + if CheckObj(obj.Code) { + return "", "", errors.New(obj.Message) + } + return obj.Data.Url, obj.Data.Qrcode_key, nil +} + +// 请求登录密钥 +func getLoginKey() (string, error) { + body, err := Request("https://passport.bilibili.com/x/passport-login/web/qrcode/generate") + if err != nil { + return "", err + } + return string(body), nil +} + +// 检查扫码状态 +func checkLoginStatus(qrcode_key string) (string, *[]*http.Cookie, error) { + // Original code manually handles cookies from response. + // My Request function returns body, error. It does not return the response object to get cookies. + // We need to modify Request to return (body, cookies, error) or exposing response? + // Or handle this case specifically. + // The requirement "unify request methods" suggests using the standard way. + // But getting cookies from response is a specific requirement here. + + // Let's implement checkLoginStatus manually here using http.Client to preserve functionality, + // OR modify Request to return *http.Response (and let user read body)? + // Or maybe a specialized request in requests.go? + + // Since this is a specific case (polling login status and getting Set-Cookie), + // I will keep the http.Client usage but try to make it cleaner or just adapt it slightly. + // Actually, the user asked to "classify request methods and unify to SAME call method (closure call)". + // If I can't fit it into `Request`, I should extend `Request` or add `RequestWithResponse`. + + // Let's add `RequestWithCookies` to requests.go? + // Or just use the original logic but inside this file? + // The prompt implies "unify" -> so I should probably make `Request` capable of returning cookies or headers. + // But `Request` currently returns `[]byte`. + + // For now, I will manually implement this using `http.Client` here but structure it similarly, + // OR I will modify `requests.go` to return `(*http.Response, []byte, error)`? That seems too heavy for simple requests. + + // I'll stick to manual implementation for this one but minimalize it, OR add a `RequestRaw` to `requests.go`. + // Let's stick to standard `http.Client` here for now to avoid over-engineering `requests.go` for one edge case, + // unless `requests.go` can be easily modified. + // Actually, `checkLoginStatus` returns `*[]*http.Cookie`. + + client := &http.Client{} + req, err := http.NewRequest("GET", "https://passport.bilibili.com/x/passport-login/web/qrcode/poll", nil) + if err != nil { + return "", nil, err + } + + q := req.URL.Query() + q.Add("qrcode_key", qrcode_key) + req.URL.RawQuery = q.Encode() + + resp, err := client.Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", nil, errors.New("Error:" + strconv.Itoa(resp.StatusCode)) + } + + cookies := resp.Cookies() + body, _ := io.ReadAll(resp.Body) // Simplify error handling as original code ignored it? No, original: _ + bodyString := string(body) + return bodyString, &cookies, nil +} + +func CheckLoginStatus(qrcode_key string) (*checkLoginReturn, *[]*http.Cookie, error) { + var obj checkLoginReturn + body, cookies, err := checkLoginStatus(qrcode_key) + if err != nil { + return nil, nil, err + } + err = decodeJson(body, &obj) + if err != nil { + return nil, nil, err + } + // 错误检查 + if CheckObj(obj.Code) { + return nil, nil, errors.New(obj.Message) + } + + return &obj, cookies, nil +} + +// 获取用户信息 +// https://socialsisteryi.github.io/bilibili-API-collect/docs/login/login_info.html +func (accountInf *AccountInformation) GetUserInf(sessdata string) error { + body, err := Request("https://api.bilibili.com/x/web-interface/nav", WithSESSDATA(sessdata)) + if err != nil { + return err + } + bodyJson := string(body) + + // 错误检查 + if CheckObj(int(gjson.Get(bodyJson, "code").Int())) { + return errors.New(gjson.Get(bodyJson, "message").String()) + } + + accountInf.Avatar = gjson.Get(bodyJson, "data.face").String() + accountInf.Name = gjson.Get(bodyJson, "data.uname").String() + + return nil +} + +// collect.go API + +func getFavList(id, ps, pn, sessdata string) (string, error) { + params := map[string]string{ + "media_id": id, + "ps": ps, + "pn": pn, + "platform": "web", + } + body, err := Request("https://api.bilibili.com/x/v3/fav/resource/list", WithSESSDATA(sessdata), WithParams(params)) + if err != nil { + return "", err + } + return string(body), nil +} + +func GetFavListObj(id, sessdata string, ps, pn int) (*FavList, error) { + var obj FavList + body, err := getFavList(id, strconv.Itoa(ps), strconv.Itoa(pn), sessdata) + if err != nil { + return nil, err + } + err = decodeJson(body, &obj) + if err != nil { + return nil, err + } + // 错误检查 + if CheckObj(obj.Code) { + return nil, errors.New(obj.Message) + } + return &obj, nil +} + +// 获取用户收藏的收藏夹 +func (collects *Collects) GetFavCollect(sessdata string, ps, pn int) error { + json, err := getUserfavoritesCollect(sessdata, strconv.Itoa(collects.UserMid), strconv.Itoa(ps), strconv.Itoa(pn)) + if err != nil { + return err + } + + // 错误检查 + if CheckObj(int(gjson.Get(json, "code").Int())) { + return errors.New(gjson.Get(json, "message").String()) + } + + collects.Count = int(gjson.Get(json, "data.count").Int()) + pageCount := collects.Count + + if collects.Count/20 >= pn { + pageCount = 20 + } else { + pageCount = collects.Count % 20 + } + + for i := 0; i < pageCount; i++ { + meta := new(meta) + meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int()) + meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int()) + meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int()) + meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String() + meta.Cover = gjson.Get(json, "data.list."+strconv.Itoa(i)+".cover").String() + meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int()) + collects.List = append(collects.List, *meta) + } + + return nil +} + +// 获取用户收藏的收藏夹 +func getUserfavoritesCollect(sessdata, mid, pageSize, pageNumber string) (string, error) { + params := map[string]string{ + "ps": pageSize, + "pn": pageNumber, + "up_mid": mid, + "platform": "web", + } + body, err := Request("https://api.bilibili.com/x/v3/fav/folder/collected/list", WithSESSDATA(sessdata), WithParams(params)) + if err != nil { + return "", err + } + return string(body), nil +} + +// 获取用户创建的收藏夹 +func (collects *Collects) GetUsersCollect(sessdata string) error { + json, err := getUsersCollect(sessdata, strconv.Itoa(collects.UserMid)) + if err != nil { + return err + } + + // 错误检查 + if CheckObj(int(gjson.Get(json, "code").Int())) { + return errors.New(gjson.Get(json, "message").String()) + } + + collects.Count = int(gjson.Get(json, "data.count").Int()) + for i := 0; i < collects.Count; i++ { + meta := new(meta) + meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int()) + meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int()) + meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int()) + meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String() + meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int()) + collects.List = append(collects.List, *meta) + } + + return nil +} + +// 获取用户创建的收藏夹 +func getUsersCollect(sessdata, mid string) (string, error) { + params := map[string]string{ + "up_mid": mid, + "platform": "web", + } + body, err := Request("https://api.bilibili.com/x/v3/fav/folder/created/list-all", WithSESSDATA(sessdata), WithParams(params)) + if err != nil { + return "", err + } + return string(body), nil +} + +// wbi.go API + +func getWbiKeys() (string, string) { + // REPLACEMENT calling Request: + body, err := Request("https://api.bilibili.com/x/web-interface/nav") + if err != nil { + return "", "" + } + + json := string(body) + imgURL := gjson.Get(json, "data.wbi_img.img_url").String() + subURL := gjson.Get(json, "data.wbi_img.sub_url").String() + // Check if imgURL/subURL are empty to avoid panic on Split? Original code didn't check. + if imgURL == "" || subURL == "" { + return "", "" + } + + return parseWbiKeys(imgURL, subURL) +} + +func parseWbiKeys(imgURL, subURL string) (string, string) { + imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0] + subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0] + return imgKey, subKey +} + +// compilation.go API + +func getCompliation(mid, sid, ps, pn string) (string, error) { + params := map[string]string{ + "mid": mid, + "season_id": sid, + "page_size": ps, + "page_num": pn, + } + // Original code sets referer manually, Request() does it too. + body, err := Request("https://api.bilibili.com/x/polymer/web-space/seasons_archives_list", WithParams(params)) + if err != nil { + return "", err + } + return string(body), nil +} + +func GetCompliationObj(mid, sid, ps, pn int) (*CompliationInformation, error) { + var obj CompliationInformation + body, err := getCompliation(strconv.Itoa(mid), strconv.Itoa(sid), strconv.Itoa(ps), strconv.Itoa(pn)) + if err != nil { + return nil, err + } + err = decodeJson(body, &obj) + if err != nil { + return nil, err + } + + // 错误检查 + if CheckObj(obj.Code) { + return nil, errors.New(obj.Message) + } + return &obj, nil +} + +// profile.go API + +// 获取用户投稿列表 +// https://socialsisteryi.github.io/bilibili-API-collect/docs/user/space.html#%E6%9F%A5%E8%AF%A2%E7%94%A8%E6%88%B7%E6%8A%95%E7%A8%BF%E8%A7%86%E9%A2%91%E6%98%8E%E7%BB%86 +func GetProfileVideo(mid, pn, ps, sessdata string) (string, error) { + params := map[string]string{ + "mid": mid, + "order": "pubdate", + "pn": pn, + "ps": ps, + } + // This uses Wbi signing! + body, err := Request("https://api.bilibili.com/x/space/wbi/arc/search", WithParams(params), WithWbi(), WithSESSDATA(sessdata)) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/bilibili/collect.go b/bilibili/collect.go index db72fda..367e4b6 100644 --- a/bilibili/collect.go +++ b/bilibili/collect.go @@ -1,14 +1,5 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "strconv" - - "github.com/tidwall/gjson" -) - // 用于获取收藏夹基本信息的函数 // 传入收藏夹 ID ,ps 单页大小, pn 页码 // 获得如下结构体 @@ -36,62 +27,7 @@ type FavList struct { } } -func getFavList(id, ps, pn, sessdata string) (string, error) { - - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/resource/list", nil) - if err != nil { - return "", err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("media_id", id) // 每页项数 - q.Add("ps", ps) // 页码 - q.Add("pn", pn) // 页码+ - q.Add("platform", "web") // 平台 - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} - -func GetFavListObj(id, sessdata string, ps, pn int) (*FavList, error) { - var obj FavList - body, err := getFavList(id, strconv.Itoa(ps), strconv.Itoa(pn), sessdata) - if err != nil { - return nil, err - } - err = decodeJson(body, &obj) - if err != nil { - return nil, err - } - // 错误检查 - if CheckObj(obj.Code) { - return nil, errors.New(obj.Message) - } - return &obj, nil -} +// Used to be getFavList here. Moved to api.go // 获取用户创建的收藏夹 type Collects struct { @@ -108,141 +44,4 @@ type meta struct { MediaCount int `json:"media_count"` } -// 获取用户收藏的收藏夹 -func (collects *Collects) GetFavCollect(sessdata string, ps, pn int) error { - json, err := getUserfavoritesCollect(sessdata, strconv.Itoa(collects.UserMid), strconv.Itoa(ps), strconv.Itoa(pn)) - if err != nil { - return err - } - - // 错误检查 - if CheckObj(int(gjson.Get(json, "code").Int())) { - return errors.New(gjson.Get(json, "message").String()) - } - - collects.Count = int(gjson.Get(json, "data.count").Int()) - pageCount := collects.Count - - if collects.Count/20 >= pn { - pageCount = 20 - } else { - pageCount = collects.Count % 20 - } - - for i := 0; i < pageCount; i++ { - meta := new(meta) - meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int()) - meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int()) - meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int()) - meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String() - meta.Cover = gjson.Get(json, "data.list."+strconv.Itoa(i)+".cover").String() - meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int()) - collects.List = append(collects.List, *meta) - } - - return nil -} - -// 获取用户收藏的收藏夹 -func getUserfavoritesCollect(sessdata, mid, pageSize, pageNumber string) (string, error) { - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/folder/collected/list", nil) - if err != nil { - return "", err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("ps", pageSize) // 每页项数 - q.Add("pn", pageNumber) // 页码 - q.Add("up_mid", mid) // 用户 mid - q.Add("platform", "web") // 平台 - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} - -// 获取用户创建的收藏夹 -func (collects *Collects) GetUsersCollect(sessdata string) error { - json, err := getUsersCollect(sessdata, strconv.Itoa(collects.UserMid)) - if err != nil { - return err - } - - // 错误检查 - if CheckObj(int(gjson.Get(json, "code").Int())) { - return errors.New(gjson.Get(json, "message").String()) - } - - collects.Count = int(gjson.Get(json, "data.count").Int()) - for i := 0; i < collects.Count; i++ { - meta := new(meta) - meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int()) - meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int()) - meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int()) - meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String() - meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int()) - collects.List = append(collects.List, *meta) - } - - return nil -} - -// 获取用户创建的收藏夹 -func getUsersCollect(sessdata, mid string) (string, error) { - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/folder/created/list-all", nil) - if err != nil { - return "", err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("up_mid", mid) // 用户 mid - q.Add("platform", "web") // 平台 - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} +// Used to be GetFavCollect, getUserfavoritesCollect, GetUsersCollect, getUsersCollect here. Moved to api.go diff --git a/bilibili/compilation.go b/bilibili/compilation.go index 7b83d69..211942f 100644 --- a/bilibili/compilation.go +++ b/bilibili/compilation.go @@ -1,12 +1,5 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "strconv" -) - // 用于获取收藏夹基本信息的函数 // 传入收藏夹 ID ,ps 单页大小, pn 页码 type CompliationInformation struct { @@ -27,61 +20,4 @@ type CompliationInformation struct { } } -func getCompliation(mid, sid, ps, pn string) (string, error) { - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/polymer/web-space/seasons_archives_list", nil) - if err != nil { - return "", err - } - - // // 添加 Cookie 到请求头 - // if sessdata != "" { - // req.Header.Add("Cookie", "SESSDATA="+sessdata) - // } - req.Header.Set("referer", "https://www.bilibili.com") - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0") - - // 设置 URL 参数 - q := req.URL.Query() - q.Add("mid", mid) - q.Add("season_id", sid) - q.Add("page_size", ps) - q.Add("page_num", pn) - req.URL.RawQuery = q.Encode() - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} - -func GetCompliationObj(mid, sid, ps, pn int) (*CompliationInformation, error) { - var obj CompliationInformation - body, err := getCompliation(strconv.Itoa(mid), strconv.Itoa(sid), strconv.Itoa(ps), strconv.Itoa(pn)) - if err != nil { - return nil, err - } - err = decodeJson(body, &obj) - if err != nil { - return nil, err - } - - // 错误检查 - if CheckObj(obj.Code) { - return nil, errors.New(obj.Message) - } - return &obj, nil -} +// Used to be getCompliation and GetCompliationObj here. Moved to api.go diff --git a/bilibili/login.go b/bilibili/login.go index 59a08f8..b6ce1de 100644 --- a/bilibili/login.go +++ b/bilibili/login.go @@ -1,14 +1,5 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "strconv" - - "github.com/tidwall/gjson" -) - // 登录密钥请求返回内容 type GetLoginKeyReturn struct { Code int `json:"code"` @@ -19,36 +10,7 @@ type GetLoginKeyReturn struct { } } -// 获取登录密钥 -func GetLoginKey() (string, string, error) { - var obj GetLoginKeyReturn - body, err := getLoginKey() - if err != nil { - return "", "", err - } - err = decodeJson(body, &obj) - if err != nil { - return "", "", err - } - // 错误检查 - if CheckObj(obj.Code) { - return "", "", errors.New(obj.Message) - } - return obj.Data.Url, obj.Data.Qrcode_key, nil -} - -// 请求登录密钥 -func getLoginKey() (string, error) { - resp, err := http.Get("https://passport.bilibili.com/x/passport-login/web/qrcode/generate") - if err != nil { - return "", err - } - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - defer resp.Body.Close() - return bodyString, nil -} +// Used to be GetLoginKey and getLoginKey here. Moved to api.go // 用于检查扫码状态和获取 cookie 的函数 type checkLoginReturn struct { @@ -63,61 +25,7 @@ type checkLoginReturn struct { } } -// 检查扫码状态 -func checkLoginStatus(qrcode_key string) (string, *[]*http.Cookie, error) { - // 创建一个 HTTP 客户端 - client := &http.Client{} - - // 创建一个 GET 请求 - req, err := http.NewRequest("GET", "https://passport.bilibili.com/x/passport-login/web/qrcode/poll", nil) - if err != nil { - return "", nil, err - } - - // 添加参数到请求的查询字符串 - q := req.URL.Query() - q.Add("qrcode_key", qrcode_key) - req.URL.RawQuery = q.Encode() - - // 发送请求并获取响应 - resp, err := client.Do(req) - if err != nil { - return "", nil, err - } - defer resp.Body.Close() - - // 检查响应状态码 - if resp.StatusCode != http.StatusOK { - return "", nil, errors.New("Error:" + strconv.Itoa(resp.StatusCode)) - } - - // 读取 Set-Cookie 头部信息 - cookies := resp.Cookies() - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - defer resp.Body.Close() - return bodyString, &cookies, nil -} - -func CheckLoginStatus(qrcode_key string) (*checkLoginReturn, *[]*http.Cookie, error) { - var obj checkLoginReturn - body, cookies, err := checkLoginStatus(qrcode_key) - if err != nil { - return nil, nil, err - } - err = decodeJson(body, &obj) - if err != nil { - return nil, nil, err - } - // 错误检查 - if CheckObj(obj.Code) { - return nil, nil, errors.New(obj.Message) - } - - return &obj, cookies, nil -} +// Used to be checkLoginStatus here. Moved to api.go // TODO: 与登录部分整合结构体 type AccountInformation struct { @@ -125,45 +33,4 @@ type AccountInformation struct { Name string `json:"name"` } -// 获取用户信息 -// https://socialsisteryi.github.io/bilibili-API-collect/docs/login/login_info.html -func (accountInf *AccountInformation) GetUserInf(sessdata string) error { - - // 创建请求 - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil) - if err != nil { - return err - } - - // 添加 Cookie 到请求头 - if sessdata != "" { - req.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyJson := string(body) - - // 错误检查 - if CheckObj(int(gjson.Get(bodyJson, "code").Int())) { - return errors.New(gjson.Get(bodyJson, "message").String()) - } - - accountInf.Avatar = gjson.Get(bodyJson, "data.face").String() - accountInf.Name = gjson.Get(bodyJson, "data.uname").String() - - return nil -} +// Used to be GetUserInf here. Moved to api.go diff --git a/bilibili/profile.go b/bilibili/profile.go index 079ff18..b4a05b0 100644 --- a/bilibili/profile.go +++ b/bilibili/profile.go @@ -1,62 +1,3 @@ package bilibili -import ( - "errors" - "io" - "net/http" - "strconv" -) - -// 获取用户投稿列表 -// https://socialsisteryi.github.io/bilibili-API-collect/docs/user/space.html#%E6%9F%A5%E8%AF%A2%E7%94%A8%E6%88%B7%E6%8A%95%E7%A8%BF%E8%A7%86%E9%A2%91%E6%98%8E%E7%BB%86 -func GetProfileVideo(mid, pn, ps, sessdata string) (string, error) { - // 创建请求 - request, err := http.NewRequest("GET", "https://api.bilibili.com/x/space/wbi/arc/search", nil) - if err != nil { - return "", err - } - - // 设置 URL 参数 - q := request.URL.Query() - q.Add("mid", mid) - q.Add("order", "pubdate") - q.Add("pn", pn) - q.Add("ps", ps) - request.URL.RawQuery = q.Encode() - - signedUrl, err := WbiSignURLParams(request.URL.String()) - if err != nil { - return "", errors.New("Wbi Sign Error: " + err.Error()) - } - - signedRequest, err := http.NewRequest("GET", signedUrl, nil) - if err != nil { - return "", errors.New("New Signed Request Error: " + err.Error()) - } - - signedRequest.Header.Set("referer", "https://www.bilibili.com") - signedRequest.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0") - - // 添加 Cookie 到请求头 - if sessdata != "" { - signedRequest.Header.Add("Cookie", "SESSDATA="+sessdata) - } - - // 创建 HTTP 客户端并发送请求 - client := &http.Client{} - resp, err := client.Do(signedRequest) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // 检查响应状态 - if resp.StatusCode != http.StatusOK { - return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode)) - } - - // 将 body 转为字符串并返回 - body, _ := io.ReadAll(resp.Body) - bodyString := string(body) - return bodyString, nil -} +// Used to be GetProfileVideo here. Moved to api.go diff --git a/bilibili/requests.go b/bilibili/requests.go new file mode 100644 index 0000000..1db74cb --- /dev/null +++ b/bilibili/requests.go @@ -0,0 +1,147 @@ +package bilibili + +import ( + "errors" + "io" + "net/http" + "net/url" + "strconv" + "time" +) + +type requestOption func(*requestConfig) + +type requestConfig struct { + method string + url string + params map[string]string + headers map[string]string + cookies map[string]string + useWbi bool +} + +func defaultRequestConfig(rawURL string) *requestConfig { + return &requestConfig{ + method: "GET", + url: rawURL, + params: make(map[string]string), + headers: make(map[string]string), + cookies: make(map[string]string), + useWbi: false, + } +} + +// WithMethod sets the HTTP method +func WithMethod(method string) requestOption { + return func(c *requestConfig) { + c.method = method + } +} + +// WithParams adds query parameters +func WithParams(params map[string]string) requestOption { + return func(c *requestConfig) { + for k, v := range params { + c.params[k] = v + } + } +} + +// WithHeaders adds HTTP headers +func WithHeaders(headers map[string]string) requestOption { + return func(c *requestConfig) { + for k, v := range headers { + c.headers[k] = v + } + } +} + +// WithCookies adds cookies +func WithCookies(cookies map[string]string) requestOption { + return func(c *requestConfig) { + for k, v := range cookies { + c.cookies[k] = v + } + } +} + +// WithSESSDATA is a helper for SESSDATA cookie +func WithSESSDATA(sessdata string) requestOption { + return func(c *requestConfig) { + if sessdata != "" { + c.cookies["SESSDATA"] = sessdata + } + } +} + +// WithWbi enables Wbi signing +func WithWbi() requestOption { + return func(c *requestConfig) { + c.useWbi = true + } +} + +// Request sends an HTTP request with the given options +func Request(rawURL string, options ...requestOption) ([]byte, error) { + config := defaultRequestConfig(rawURL) + for _, option := range options { + option(config) + } + + reqURL, err := url.Parse(config.url) + if err != nil { + return nil, err + } + + q := reqURL.Query() + for k, v := range config.params { + q.Add(k, v) + } + reqURL.RawQuery = q.Encode() + + finalURL := reqURL.String() + if config.useWbi { + signedUrl, err := WbiSignURLParams(finalURL) + if err != nil { + return nil, errors.New("Wbi Sign Error: " + err.Error()) + } + finalURL = signedUrl + } + + req, err := http.NewRequest(config.method, finalURL, nil) + if err != nil { + return nil, err + } + + // Default Headers + req.Header.Set("User-Agent", GetRandomUA()) + req.Header.Set("Referer", "https://www.bilibili.com") + + for k, v := range config.headers { + req.Header.Set(k, v) + } + + for k, v := range config.cookies { + req.AddCookie(&http.Cookie{Name: k, Value: v}) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("Error: " + strconv.Itoa(resp.StatusCode)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} diff --git a/bilibili/ua.go b/bilibili/ua.go new file mode 100644 index 0000000..0e717a1 --- /dev/null +++ b/bilibili/ua.go @@ -0,0 +1,26 @@ +package bilibili + +import ( + "math/rand" + "time" +) + +var userAgents = []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// GetRandomUA returns a random User-Agent string +func GetRandomUA() string { + return userAgents[rand.Intn(len(userAgents))] +} diff --git a/bilibili/wbi.go b/bilibili/wbi.go index 86d03d9..c8b38f0 100644 --- a/bilibili/wbi.go +++ b/bilibili/wbi.go @@ -3,17 +3,12 @@ package bilibili import ( "crypto/md5" "encoding/hex" - "fmt" - "io" - "net/http" "net/url" "sort" "strconv" "strings" "sync" "time" - - "github.com/tidwall/gjson" ) var ( @@ -113,31 +108,3 @@ func getWbiKeysCached() (string, string) { subKeyI, _ := cache.Load("subKey") return imgKeyI.(string), subKeyI.(string) } - -func getWbiKeys() (string, string) { - client := &http.Client{} - req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil) - if err != nil { - fmt.Printf("Error creating request: %s", err) - return "", "" - } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Referer", "https://www.bilibili.com/") - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Error sending request: %s", err) - return "", "" - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Printf("Error reading response: %s", err) - return "", "" - } - json := string(body) - imgURL := gjson.Get(json, "data.wbi_img.img_url").String() - subURL := gjson.Get(json, "data.wbi_img.sub_url").String() - imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0] - subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0] - return imgKey, subKey -} diff --git a/frontend/package.json b/frontend/package.json index 543fe75..4d0bfd1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,19 +1,24 @@ { - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "vue": "^3.2.37", - "@varlet/ui": "^3.6.1" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^3.0.3", - "vite": "^3.0.7" - } + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "@varlet/ui": "^3.13.1", + "tailwindcss": "^4.1.18", + "vue": "^3.5.27" + }, + "devDependencies": { + "@types/node": "^25.0.10", + "@vitejs/plugin-vue": "^6.0.3", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vue-tsc": "^3.2.3" + } } \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 263dc19..15016a0 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -4a755aa8d82c1f44ce8a545fab0d3126 \ No newline at end of file +7248408b271493ee1976d8c61658cc84 \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index f65cca1..6818440 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,3 +1,7 @@ -body, html, #root { - font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", "Helvetica Neue", Arial, sans-serif; -} +@import "tailwindcss"; + +body, +html, +#root { + font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", "Helvetica Neue", Arial, sans-serif; +} \ No newline at end of file diff --git a/frontend/src/components/collect_download.vue b/frontend/src/components/collect_download.vue index d828b27..7fa096f 100644 --- a/frontend/src/components/collect_download.vue +++ b/frontend/src/components/collect_download.vue @@ -13,7 +13,7 @@ -