diff --git a/bilibili/.gitignore b/bilibili/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/bilibili/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/bilibili/api.go b/bilibili/api.go index df2abd1..8d17959 100644 --- a/bilibili/api.go +++ b/bilibili/api.go @@ -75,9 +75,19 @@ func LoadDynamicDetail(str string) (card DynamicCard, err error) { } // GetDynamicDetail 用动态id查动态信息 -func GetDynamicDetail(dynamicIDStr string) (card DynamicCard, err error) { +func GetDynamicDetail(cookiecfg *CookieConfig, dynamicIDStr string) (card DynamicCard, err error) { var data []byte - data, err = web.GetData(fmt.Sprintf(DynamicDetailURL, dynamicIDStr)) + data, err = web.RequestDataWithHeaders(web.NewDefaultClient(), fmt.Sprintf(DynamicDetailURL, dynamicIDStr), "GET", func(req *http.Request) error { + if cookiecfg != nil { + cookie := "" + cookie, err = cookiecfg.Load() + if err != nil { + return err + } + req.Header.Add("cookie", cookie) + } + return nil + }, nil) if err != nil { return } @@ -183,3 +193,30 @@ func GetVideoInfo(id string) (card Card, err error) { err = json.Unmarshal(binary.StringToBytes(gjson.ParseBytes(data).Get("data").Raw), &card) return } + +// GetVideoSummary 用av或bv查看AI视频总结 +func GetVideoSummary(id string) (videoSummary VideoSummary, err error) { + var ( + data []byte + card Card + ) + _, err = strconv.Atoi(id) + if err == nil { + data, err = web.GetData(fmt.Sprintf(VideoInfoURL, id, "")) + } else { + data, err = web.GetData(fmt.Sprintf(VideoInfoURL, "", id)) + } + if err != nil { + return + } + err = json.Unmarshal(binary.StringToBytes(gjson.ParseBytes(data).Get("data").Raw), &card) + if err != nil { + return + } + data, err = web.GetData(SignURL(fmt.Sprintf(VideoSummaryURL, card.BvID, card.CID))) + if err != nil { + return + } + err = json.Unmarshal(data, &videoSummary) + return +} diff --git a/bilibili/api_test.go b/bilibili/api_test.go index 9cff8ae..262fcb1 100644 --- a/bilibili/api_test.go +++ b/bilibili/api_test.go @@ -1,6 +1,8 @@ package bilibili -import "testing" +import ( + "testing" +) func TestGetAllGuard(t *testing.T) { guardUser, err := GetAllGuard("628537") @@ -9,3 +11,20 @@ func TestGetAllGuard(t *testing.T) { } t.Logf("%+v\n", guardUser) } + +func TestGetVideoSummary(t *testing.T) { + videoSummary, err := GetVideoSummary("BV1ju4y1s7kn") + if err != nil { + t.Fatal(err) + } + t.Logf("%+v\n", videoSummary) +} + +func TestGetDynamicDetail(t *testing.T) { + cfg := NewCookieConfig("config.json") + detail, err := GetDynamicDetail(cfg, "851252197280710664") + if err != nil { + t.Fatal(err) + } + t.Logf("%+v\n", detail) +} diff --git a/bilibili/types.go b/bilibili/types.go index 1f9a6d4..91f8a61 100644 --- a/bilibili/types.go +++ b/bilibili/types.go @@ -45,6 +45,10 @@ const ( DanmakuURL = "https://danmakus.com/user/%v" // AllGuardURL 查询所有舰长,提督,总督 AllGuardURL = "https://api.vtbs.moe/v1/guard/all" + // VideoSummaryURL AI视频总结 + VideoSummaryURL = "https://api.bilibili.com/x/web-interface/view/conclusion/get?bvid=%v&cid=%v" + // NavURL 导航URL + NavURL = "https://api.bilibili.com/x/web-interface/nav" ) // DynamicCard 总动态结构体,包括desc,card @@ -79,6 +83,7 @@ type Card struct { AID any `json:"aid"` BvID any `json:"bvid"` Dynamic any `json:"dynamic"` + CID int `json:"cid"` Pic string `json:"pic"` Title string `json:"title"` ID int `json:"id"` @@ -350,6 +355,32 @@ type Danmakusuki struct { } `json:"data"` } +// VideoSummary AI视频总结结构体 +type VideoSummary struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + Code int `json:"code"` + ModelResult struct { + ResultType int `json:"result_type"` + Summary string `json:"summary"` + Outline []struct { + Title string `json:"title"` + PartOutline []struct { + Timestamp int `json:"timestamp"` + Content string `json:"content"` + } `json:"part_outline"` + Timestamp int `json:"timestamp"` + } `json:"outline"` + } `json:"model_result"` + Stid string `json:"stid"` + Status int `json:"status"` + LikeNum int `json:"like_num"` + DislikeNum int `json:"dislike_num"` + } `json:"data"` +} + // CookieConfig 配置结构体 type CookieConfig struct { BilibiliCookie string `json:"bilibili_cookie"` diff --git a/bilibili/wbi.go b/bilibili/wbi.go new file mode 100644 index 0000000..1a67dd0 --- /dev/null +++ b/bilibili/wbi.go @@ -0,0 +1,117 @@ +package bilibili + +import ( + "crypto/md5" + "encoding/hex" + "net/url" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/FloatTech/floatbox/binary" + "github.com/FloatTech/floatbox/web" + "github.com/RomiChan/syncx" + "github.com/tidwall/gjson" +) + +var ( + mixinKeyEncTab = []int{ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52, + } + cache syncx.Map[string, string] + lastUpdateTime time.Time + replacements = [...]string{"!", "'", "(", ")", "*"} +) + +// SignURL wbi签名包装 https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md +func SignURL(urlStr string) string { + urlObj, _ := url.Parse(urlStr) + imgKey, subKey := getWbiKeysCached() + query := urlObj.Query() + params := map[string]string{} + for k, v := range query { + if len(v) > 0 { + params[k] = v[0] + } + } + newParams := wbiSign(params, imgKey, subKey) + for k, v := range newParams { + query.Set(k, v) + } + urlObj.RawQuery = query.Encode() + newURL := urlObj.String() + return newURL +} + +func getMixinKey(orig string) string { + var str strings.Builder + t := 0 + for _, v := range mixinKeyEncTab { + if v < len(orig) { + str.WriteByte(orig[v]) + t++ + } + if t > 31 { + break + } + } + return str.String() +} + +func wbiSign(params map[string]string, imgKey string, subKey string) map[string]string { + mixinKey := getMixinKey(imgKey + subKey) + currTime := strconv.FormatInt(time.Now().Unix(), 10) + params["wts"] = currTime + // Sort keys + keys := make([]string, 0, len(params)) + for k, v := range params { + keys = append(keys, k) + for _, old := range replacements { + v = strings.ReplaceAll(v, old, "") + } + params[k] = v + } + sort.Strings(keys) + h := md5.New() + for k, v := range keys { + h.Write([]byte(v)) + h.Write([]byte{'='}) + h.Write([]byte(params[v])) + if k < len(keys)-1 { + h.Write([]byte{'&'}) + } + } + h.Write([]byte(mixinKey)) + params["w_rid"] = hex.EncodeToString(h.Sum(make([]byte, 0, md5.Size))) + return params +} + +func getWbiKeysCached() (string, string) { + if time.Since(lastUpdateTime).Minutes() > 10 { + imgKey, subKey := getWbiKeys() + cache.Store("imgKey", imgKey) + cache.Store("subKey", subKey) + lastUpdateTime = time.Now() + return imgKey, subKey + } + imgKeyI, _ := cache.Load("imgKey") + subKeyI, _ := cache.Load("subKey") + return imgKeyI, subKeyI +} + +func getWbiKeys() (string, string) { + data, _ := web.GetData(NavURL) + json := binary.BytesToString(data) + imgURL := gjson.Get(json, "data.wbi_img.img_url").String() + subURL := gjson.Get(json, "data.wbi_img.sub_url").String() + imgKey := imgURL[strings.LastIndex(imgURL, "/")+1:] + imgKey = strings.TrimSuffix(imgKey, filepath.Ext(imgKey)) + subKey := subURL[strings.LastIndex(subURL, "/")+1:] + subKey = strings.TrimSuffix(subKey, filepath.Ext(subKey)) + return imgKey, subKey +}