dd8eaef97d
- Reorganize code into cmd/bilidown and internal/ packages - Rename client/ to web/ for frontend source - Remove systray dependency for headless web service - Embed static files into binary using go:embed - Update import paths to use internal/ prefix - Update .gitignore with common patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
4.5 KiB
Go
178 lines
4.5 KiB
Go
package bilibili
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
|
|
"bilidown/internal/util"
|
|
)
|
|
|
|
type BiliClient struct {
|
|
SESSDATA string
|
|
}
|
|
|
|
// SimpleGET 简单的 GET 请求
|
|
func (client *BiliClient) SimpleGET(_url string, params map[string]string) (*http.Response, error) {
|
|
values := url.Values{}
|
|
for k, v := range params {
|
|
values.Set(k, v)
|
|
}
|
|
_client := http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyURL(nil),
|
|
},
|
|
}
|
|
request, err := http.NewRequest("GET", _url+"?"+values.Encode(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Header = client.MakeHeader()
|
|
return _client.Do(request)
|
|
}
|
|
|
|
// MakeHeader 生成请求头
|
|
func (client *BiliClient) MakeHeader() http.Header {
|
|
header := http.Header{}
|
|
header.Set("Cookie", "SESSDATA="+client.SESSDATA)
|
|
header.Set("User-Agent", "Mozilla/5.0")
|
|
header.Set("Referer", "https://www.bilibili.com")
|
|
return header
|
|
}
|
|
|
|
// CheckLogin 检查是否已经登录
|
|
func (client *BiliClient) CheckLogin() (bool, error) {
|
|
response, err := client.SimpleGET("https://api.bilibili.com/x/space/myinfo", nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer response.Body.Close()
|
|
body := BaseResV2{}
|
|
err = json.NewDecoder(response.Body).Decode(&body)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if body.Code != 0 {
|
|
return false, errors.New(body.Message)
|
|
}
|
|
return body.Success(), nil
|
|
}
|
|
|
|
// NewQRInfo 获取登录二维码信息
|
|
func (client *BiliClient) NewQRInfo() (*QRInfo, error) {
|
|
response, err := client.SimpleGET("https://passport.bilibili.com/x/passport-login/web/qrcode/generate", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
body := BaseResV2{}
|
|
err = json.NewDecoder(response.Body).Decode(&body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if body.Code != 0 {
|
|
return nil, errors.New(body.Message)
|
|
}
|
|
qrInfo := QRInfo{}
|
|
err = json.Unmarshal(body.Data, &qrInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &qrInfo, nil
|
|
}
|
|
|
|
func (client *BiliClient) getWbiKeyRemote() (wbiKey string, err error) {
|
|
if client.SESSDATA == "" {
|
|
return "", errors.New("SESSDATA 不能为空")
|
|
}
|
|
response, err := client.SimpleGET("https://api.bilibili.com/x/web-interface/nav", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close()
|
|
body := BaseResV2{}
|
|
if err = json.NewDecoder(response.Body).Decode(&body); err != nil {
|
|
return "", err
|
|
}
|
|
var data struct {
|
|
WbiImg struct {
|
|
ImgURL string `json:"img_url"`
|
|
SubURL string `json:"sub_url"`
|
|
} `json:"wbi_img"`
|
|
}
|
|
if err = json.Unmarshal(body.Data, &data); err != nil {
|
|
return "", err
|
|
}
|
|
match := regexp.MustCompile(`/bfs/wbi/([a-z0-9]+)\.`)
|
|
imgKey := match.FindStringSubmatch(data.WbiImg.ImgURL)[1]
|
|
subKey := match.FindStringSubmatch(data.WbiImg.SubURL)[1]
|
|
if imgKey == "" || subKey == "" {
|
|
return "", errors.New("regexp.MustCompile(`/bfs/wbi/([a-z0-9])\\.`)")
|
|
}
|
|
return imgKey + subKey, nil
|
|
}
|
|
|
|
// GetQRStatus 获取二维码状态
|
|
func (client *BiliClient) GetQRStatus(qrKey string) (qrStatus *QRStatus, sessdata string, err error) {
|
|
params := map[string]string{
|
|
"qrcode_key": qrKey,
|
|
}
|
|
response, err := client.SimpleGET("https://passport.bilibili.com/x/passport-login/web/qrcode/poll", params)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer response.Body.Close()
|
|
body := BaseResV2{}
|
|
err = json.NewDecoder(response.Body).Decode(&body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if body.Code != 0 {
|
|
return nil, "", errors.New(body.Message)
|
|
}
|
|
qrStatus = &QRStatus{}
|
|
err = json.Unmarshal(body.Data, &qrStatus)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if qrStatus.Code != 0 {
|
|
return qrStatus, "", nil
|
|
}
|
|
sessdata, err = GetCookieValue(response.Cookies(), "SESSDATA")
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return qrStatus, sessdata, nil
|
|
}
|
|
|
|
// GetCookieValue 获取指定 Name 的 Cookie 值
|
|
func GetCookieValue(cookies []*http.Cookie, name string) (string, error) {
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == name {
|
|
return cookie.Value, nil
|
|
}
|
|
}
|
|
return "", errors.New("cookie with name " + name + " not found")
|
|
}
|
|
|
|
// SaveSessdata 保存 SESSDATA
|
|
func SaveSessdata(db *sql.DB, sessdata string) error {
|
|
util.SqliteLock.Lock()
|
|
_, err := db.Exec(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES ("sessdata", ?)`, sessdata)
|
|
util.SqliteLock.Unlock()
|
|
return err
|
|
}
|
|
|
|
// GetSessdata 获取 SESSDATA
|
|
func GetSessdata(db *sql.DB) (string, error) {
|
|
util.SqliteLock.Lock()
|
|
row := db.QueryRow(`SELECT "value" FROM "field" WHERE "name" = "sessdata"`)
|
|
util.SqliteLock.Unlock()
|
|
var sessdata string
|
|
err := row.Scan(&sessdata)
|
|
return sessdata, err
|
|
}
|