|
package baiduphoto |
|
|
|
import ( |
|
"context" |
|
"encoding/hex" |
|
"fmt" |
|
"net/http" |
|
"strconv" |
|
"strings" |
|
"unicode" |
|
|
|
"github.com/alist-org/alist/v3/drivers/base" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/go-resty/resty/v2" |
|
) |
|
|
|
const ( |
|
API_URL = "https://photo.baidu.com/youai" |
|
USER_API_URL = API_URL + "/user/v1" |
|
ALBUM_API_URL = API_URL + "/album/v1" |
|
FILE_API_URL_V1 = API_URL + "/file/v1" |
|
FILE_API_URL_V2 = API_URL + "/file/v2" |
|
) |
|
|
|
func (d *BaiduPhoto) Request(client *resty.Client, furl string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) { |
|
req := client.R(). |
|
|
|
SetHeader("Cookie", d.Cookie) |
|
if callback != nil { |
|
callback(req) |
|
} |
|
if resp != nil { |
|
req.SetResult(resp) |
|
} |
|
res, err := req.Execute(method, furl) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
erron := utils.Json.Get(res.Body(), "errno").ToInt() |
|
switch erron { |
|
case 0: |
|
break |
|
case 50805: |
|
return nil, fmt.Errorf("you have joined album") |
|
case 50820: |
|
return nil, fmt.Errorf("no shared albums found") |
|
case 50100: |
|
return nil, fmt.Errorf("illegal title, only supports 50 characters") |
|
|
|
|
|
|
|
|
|
default: |
|
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron) |
|
} |
|
return res, nil |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (d *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) { |
|
return d.Request(base.RestyClient, furl, http.MethodGet, callback, resp) |
|
} |
|
|
|
func (d *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) { |
|
return d.Request(base.RestyClient, furl, http.MethodPost, callback, resp) |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) { |
|
var cursor string |
|
for { |
|
var resp FileListResp |
|
_, err = d.Get(FILE_API_URL_V1+"/list", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetQueryParams(map[string]string{ |
|
"need_thumbnail": "1", |
|
"need_filter_hidden": "0", |
|
"cursor": cursor, |
|
}) |
|
}, &resp) |
|
if err != nil { |
|
return |
|
} |
|
|
|
files = append(files, resp.List...) |
|
if !resp.HasNextPage() { |
|
return |
|
} |
|
cursor = resp.Cursor |
|
} |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) DeleteFile(ctx context.Context, file *File) error { |
|
_, err := d.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) { |
|
req.SetContext(ctx) |
|
req.SetQueryParams(map[string]string{ |
|
"fsid_list": fmt.Sprintf("[%d]", file.Fsid), |
|
}) |
|
}, nil) |
|
return err |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) { |
|
var cursor string |
|
for { |
|
var resp AlbumListResp |
|
_, err = d.Get(ALBUM_API_URL+"/list", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetQueryParams(map[string]string{ |
|
"need_amount": "1", |
|
"limit": "100", |
|
"cursor": cursor, |
|
}) |
|
}, &resp) |
|
if err != nil { |
|
return |
|
} |
|
if albums == nil { |
|
albums = make([]Album, 0, resp.TotalCount) |
|
} |
|
|
|
cursor = resp.Cursor |
|
albums = append(albums, resp.List...) |
|
|
|
if !resp.HasNextPage() { |
|
return |
|
} |
|
} |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) GetAllAlbumFile(ctx context.Context, album *Album, passwd string) (files []AlbumFile, err error) { |
|
var cursor string |
|
for { |
|
var resp AlbumFileListResp |
|
_, err = d.Get(ALBUM_API_URL+"/listfile", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetQueryParams(map[string]string{ |
|
"album_id": album.AlbumID, |
|
"need_amount": "1", |
|
"limit": "1000", |
|
"passwd": passwd, |
|
"cursor": cursor, |
|
}) |
|
}, &resp) |
|
if err != nil { |
|
return |
|
} |
|
if files == nil { |
|
files = make([]AlbumFile, 0, resp.TotalCount) |
|
} |
|
|
|
cursor = resp.Cursor |
|
files = append(files, resp.List...) |
|
|
|
if !resp.HasNextPage() { |
|
return |
|
} |
|
} |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) CreateAlbum(ctx context.Context, name string) (*Album, error) { |
|
var resp JoinOrCreateAlbumResp |
|
_, err := d.Post(ALBUM_API_URL+"/create", func(r *resty.Request) { |
|
r.SetContext(ctx).SetResult(&resp) |
|
r.SetQueryParams(map[string]string{ |
|
"title": name, |
|
"tid": getTid(), |
|
"source": "0", |
|
}) |
|
}, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return d.GetAlbumDetail(ctx, resp.AlbumID) |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) SetAlbumName(ctx context.Context, album *Album, name string) (*Album, error) { |
|
_, err := d.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetFormData(map[string]string{ |
|
"title": name, |
|
"album_id": album.AlbumID, |
|
"tid": fmt.Sprint(album.Tid), |
|
}) |
|
}, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return renameAlbum(album, name), nil |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) DeleteAlbum(ctx context.Context, album *Album) error { |
|
_, err := d.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetFormData(map[string]string{ |
|
"album_id": album.AlbumID, |
|
"tid": fmt.Sprint(album.Tid), |
|
"delete_origin_image": BoolToIntStr(d.DeleteOrigin), |
|
}) |
|
}, nil) |
|
return err |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) DeleteAlbumFile(ctx context.Context, file *AlbumFile) error { |
|
_, err := d.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetFormData(map[string]string{ |
|
"album_id": fmt.Sprint(file.AlbumID), |
|
"tid": fmt.Sprint(file.Tid), |
|
"list": fmt.Sprintf(`[{"fsid":%d,"uk":%d}]`, file.Fsid, file.Uk), |
|
"del_origin": BoolToIntStr(d.DeleteOrigin), |
|
}) |
|
}, nil) |
|
return err |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) AddAlbumFile(ctx context.Context, album *Album, file *File) (*AlbumFile, error) { |
|
_, err := d.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetQueryParams(map[string]string{ |
|
"album_id": fmt.Sprint(album.AlbumID), |
|
"tid": fmt.Sprint(album.Tid), |
|
"list": fsidsFormatNotUk(file.Fsid), |
|
}) |
|
}, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return moveFileToAlbumFile(file, album, d.Uk), nil |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) CopyAlbumFile(ctx context.Context, file *AlbumFile) (*File, error) { |
|
var resp CopyFileResp |
|
_, err := d.Post(ALBUM_API_URL+"/copyfile", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetFormData(map[string]string{ |
|
"album_id": file.AlbumID, |
|
"tid": fmt.Sprint(file.Tid), |
|
"uk": fmt.Sprint(file.Uk), |
|
"list": fsidsFormatNotUk(file.Fsid), |
|
}) |
|
r.SetResult(&resp) |
|
}, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return copyFile(file, &resp.List[0]), nil |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) JoinAlbum(ctx context.Context, code string) (*Album, error) { |
|
var resp InviteResp |
|
_, err := d.Get(ALBUM_API_URL+"/querypcode", func(req *resty.Request) { |
|
req.SetContext(ctx) |
|
req.SetQueryParams(map[string]string{ |
|
"pcode": code, |
|
"web": "1", |
|
}) |
|
}, &resp) |
|
if err != nil { |
|
return nil, err |
|
} |
|
var resp2 JoinOrCreateAlbumResp |
|
_, err = d.Get(ALBUM_API_URL+"/join", func(req *resty.Request) { |
|
req.SetContext(ctx) |
|
req.SetQueryParams(map[string]string{ |
|
"invite_code": resp.Pdata.InviteCode, |
|
}) |
|
}, &resp2) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return d.GetAlbumDetail(ctx, resp2.AlbumID) |
|
} |
|
|
|
|
|
func (d *BaiduPhoto) GetAlbumDetail(ctx context.Context, albumID string) (*Album, error) { |
|
var album Album |
|
_, err := d.Get(ALBUM_API_URL+"/detail", func(req *resty.Request) { |
|
req.SetContext(ctx).SetResult(&album) |
|
req.SetQueryParams(map[string]string{ |
|
"album_id": albumID, |
|
}) |
|
}, &album) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return &album, nil |
|
} |
|
|
|
func (d *BaiduPhoto) linkAlbum(ctx context.Context, file *AlbumFile, args model.LinkArgs) (*model.Link, error) { |
|
headers := map[string]string{ |
|
"User-Agent": base.UserAgent, |
|
} |
|
if args.Header.Get("User-Agent") != "" { |
|
headers["User-Agent"] = args.Header.Get("User-Agent") |
|
} |
|
if !utils.IsLocalIPAddr(args.IP) { |
|
headers["X-Forwarded-For"] = args.IP |
|
} |
|
|
|
resp, err := d.Request(base.NoRedirectClient, ALBUM_API_URL+"/download", http.MethodHead, func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetHeaders(headers) |
|
r.SetQueryParams(map[string]string{ |
|
"fsid": fmt.Sprint(file.Fsid), |
|
"album_id": file.AlbumID, |
|
"tid": fmt.Sprint(file.Tid), |
|
"uk": fmt.Sprint(file.Uk), |
|
}) |
|
}, nil) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if resp.StatusCode() != 302 { |
|
return nil, fmt.Errorf("not found 302 redirect") |
|
} |
|
|
|
location := resp.Header().Get("Location") |
|
|
|
link := &model.Link{ |
|
URL: location, |
|
Header: http.Header{ |
|
"User-Agent": []string{headers["User-Agent"]}, |
|
"Referer": []string{"https://photo.baidu.com/"}, |
|
}, |
|
} |
|
return link, nil |
|
} |
|
|
|
func (d *BaiduPhoto) linkFile(ctx context.Context, file *File, args model.LinkArgs) (*model.Link, error) { |
|
headers := map[string]string{ |
|
"User-Agent": base.UserAgent, |
|
} |
|
if args.Header.Get("User-Agent") != "" { |
|
headers["User-Agent"] = args.Header.Get("User-Agent") |
|
} |
|
if !utils.IsLocalIPAddr(args.IP) { |
|
headers["X-Forwarded-For"] = args.IP |
|
} |
|
|
|
var downloadUrl struct { |
|
Dlink string `json:"dlink"` |
|
} |
|
_, err := d.Get(FILE_API_URL_V2+"/download", func(r *resty.Request) { |
|
r.SetContext(ctx) |
|
r.SetHeaders(headers) |
|
r.SetQueryParams(map[string]string{ |
|
"fsid": fmt.Sprint(file.Fsid), |
|
}) |
|
}, &downloadUrl) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
link := &model.Link{ |
|
URL: downloadUrl.Dlink, |
|
Header: http.Header{ |
|
"User-Agent": []string{headers["User-Agent"]}, |
|
"Referer": []string{"https://photo.baidu.com/"}, |
|
}, |
|
} |
|
return link, nil |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (d *BaiduPhoto) uInfo() (*UInfo, error) { |
|
var info UInfo |
|
_, err := d.Get(USER_API_URL+"/getuinfo", func(req *resty.Request) { |
|
|
|
}, &info) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return &info, nil |
|
} |
|
|
|
func DecryptMd5(encryptMd5 string) string { |
|
if _, err := hex.DecodeString(encryptMd5); err == nil { |
|
return encryptMd5 |
|
} |
|
|
|
var out strings.Builder |
|
out.Grow(len(encryptMd5)) |
|
for i, n := 0, int64(0); i < len(encryptMd5); i++ { |
|
if i == 9 { |
|
n = int64(unicode.ToLower(rune(encryptMd5[i])) - 'g') |
|
} else { |
|
n, _ = strconv.ParseInt(encryptMd5[i:i+1], 16, 64) |
|
} |
|
out.WriteString(strconv.FormatInt(n^int64(15&i), 16)) |
|
} |
|
|
|
encryptMd5 = out.String() |
|
return encryptMd5[8:16] + encryptMd5[:8] + encryptMd5[24:32] + encryptMd5[16:24] |
|
} |
|
|
|
func EncryptMd5(originalMd5 string) string { |
|
reversed := originalMd5[8:16] + originalMd5[:8] + originalMd5[24:32] + originalMd5[16:24] |
|
|
|
var out strings.Builder |
|
out.Grow(len(reversed)) |
|
for i, n := 0, int64(0); i < len(reversed); i++ { |
|
n, _ = strconv.ParseInt(reversed[i:i+1], 16, 64) |
|
n ^= int64(15 & i) |
|
if i == 9 { |
|
out.WriteRune(rune(n) + 'g') |
|
} else { |
|
out.WriteString(strconv.FormatInt(n, 16)) |
|
} |
|
} |
|
return out.String() |
|
} |
|
|