Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions drivers/pikpak/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
}

// 获取CaptchaToken
err = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/files"), d.Common.GetUserID())
err = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, d.apiURL("/drive/v1/files")), d.Common.GetUserID())
if err != nil {
return err
}
Expand Down Expand Up @@ -137,7 +137,7 @@ func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
if !d.DisableMediaLink {
queryParams["usage"] = "CACHE"
}
_, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.net/drive/v1/files/%s", file.GetID()),
_, err := d.request(fmt.Sprintf(d.apiURL("/drive/v1/files/%s"), file.GetID()),
http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(queryParams)
}, &resp)
Expand All @@ -151,13 +151,16 @@ func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
url = resp.Medias[0].Link.Url
}

// 按需把直链域名重写为用户选择的 PikPak 域名以提升播放/下载速度
url = d.rewriteDownloadURL(url)

return &model.Link{
URL: url,
}, nil
}

func (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files"), http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"kind": "drive#folder",
"parent_id": parentDir.GetID(),
Expand All @@ -168,7 +171,7 @@ func (d *PikPak) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
}

func (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchMove", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files:batchMove"), http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"ids": []string{srcObj.GetID()},
"to": base.Json{
Expand All @@ -180,7 +183,7 @@ func (d *PikPak) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
}

func (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files/"+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files/")+srcObj.GetID(), http.MethodPatch, func(req *resty.Request) {
req.SetBody(base.Json{
"name": newName,
})
Expand All @@ -189,7 +192,7 @@ func (d *PikPak) Rename(ctx context.Context, srcObj model.Obj, newName string) e
}

func (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchCopy", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files:batchCopy"), http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"ids": []string{srcObj.GetID()},
"to": base.Json{
Expand All @@ -201,7 +204,7 @@ func (d *PikPak) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}

func (d *PikPak) Remove(ctx context.Context, obj model.Obj) error {
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files:batchTrash", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files:batchTrash"), http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"ids": []string{obj.GetID()},
})
Expand All @@ -225,7 +228,7 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
}

var resp UploadTaskData
res, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
res, err := d.request(d.apiURL("/drive/v1/files"), http.MethodPost, func(req *resty.Request) {
req.SetBody(base.Json{
"kind": "drive#file",
"name": stream.GetName(),
Expand Down Expand Up @@ -275,7 +278,7 @@ func (d *PikPak) OfflineDownload(ctx context.Context, fileUrl string, parentDir
}

var resp OfflineDownloadResp
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files"), http.MethodPost, func(req *resty.Request) {
req.SetBody(requestBody)
}, &resp)

Expand All @@ -293,7 +296,7 @@ PHASE_TYPE_RUNNING, PHASE_TYPE_ERROR, PHASE_TYPE_COMPLETE, PHASE_TYPE_PENDING
*/
func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []string) ([]OfflineTask, error) {
res := make([]OfflineTask, 0)
url := "https://api-drive.mypikpak.net/drive/v1/tasks"
url := d.apiURL("/drive/v1/tasks")

if len(phase) == 0 {
phase = []string{"PHASE_TYPE_RUNNING", "PHASE_TYPE_ERROR", "PHASE_TYPE_COMPLETE", "PHASE_TYPE_PENDING"}
Expand Down Expand Up @@ -334,7 +337,7 @@ func (d *PikPak) OfflineList(ctx context.Context, nextPageToken string, phase []
}

func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, deleteFiles bool) error {
url := "https://api-drive.mypikpak.net/drive/v1/tasks"
url := d.apiURL("/drive/v1/tasks")
params := map[string]string{
"task_ids": strings.Join(taskIDs, ","),
"delete_files": strconv.FormatBool(deleteFiles),
Expand Down
6 changes: 6 additions & 0 deletions drivers/pikpak/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ type Addition struct {
CaptchaToken string `json:"captcha_token" default:""`
DeviceID string `json:"device_id" required:"false" default:""`
DisableMediaLink bool `json:"disable_media_link" default:"true"`
// API 请求域名:从内置列表选择,或用 custom_api_domain 手动填写覆盖。用于构建 api-drive.<domain> / user.<domain>
ApiDomain string `json:"api_domain" type:"select" options:"mypikpak_net,mypikpak_com,pikpak_me,pikpakdrive_com" default:"mypikpak_net"`
CustomApiDomain string `json:"custom_api_domain" help:"Custom PikPak API domain (e.g. mypikpak.net). When set, it overrides the selected api_domain."`
// 下载/播放直链域名:original=不改写;或从内置列表选择 / 用 custom_download_domain 手动填写覆盖
DownloadDomain string `json:"download_domain" type:"select" options:"original,mypikpak_net,mypikpak_com,pikpak_me,pikpakdrive_com" default:"original"`
CustomDownloadDomain string `json:"custom_download_domain" help:"Custom domain to rewrite the download/play link host to (e.g. mypikpak.net). When set, it overrides the selected download_domain. Keep 'original' (and empty) to leave the link unchanged."`
}

var config = driver.Config{
Expand Down
97 changes: 93 additions & 4 deletions drivers/pikpak/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -93,13 +94,101 @@ const (
PCSdkVersion = "8.0.3"
)

// defaultPikPakDomain is the API domain used when api_domain is left empty.
const defaultPikPakDomain = "mypikpak.net"

// pikPakDomains is the list of known PikPak domains whose host can be swapped
// on download/play links. See issue #9559.
var pikPakDomains = []string{"mypikpak.com", "mypikpak.net", "pikpak.me", "pikpakdrive.com"}

// domainOptionMap maps the built-in select option keys (which avoid dots so the
// web UI renders readable labels) to the real domains.
var domainOptionMap = map[string]string{
"mypikpak_net": "mypikpak.net",
"mypikpak_com": "mypikpak.com",
"pikpak_me": "pikpak.me",
"pikpakdrive_com": "pikpakdrive.com",
}

// resolveDomainOption turns a built-in option key into its real domain, while
// passing through any value already in domain form (manual entry / legacy).
func resolveDomainOption(v string) string {
if domain, ok := domainOptionMap[v]; ok {
return domain
}
return v
}

// getApiDomain returns the API domain to use: the manually entered
// custom_api_domain takes precedence over the selected api_domain, and falls
// back to the default when both are empty.
func (d *PikPak) getApiDomain() string {
if domain := strings.TrimSpace(d.CustomApiDomain); domain != "" {
return domain
}
if domain := strings.TrimSpace(d.ApiDomain); domain != "" {
return resolveDomainOption(domain)
}
return defaultPikPakDomain
}

// getDownloadDomain returns the domain that download/play links should be
// rewritten to, or "" when links should be kept unchanged. The manually
// entered custom_download_domain takes precedence over the selected
// download_domain; the sentinel "original" means "do not rewrite".
func (d *PikPak) getDownloadDomain() string {
if domain := strings.TrimSpace(d.CustomDownloadDomain); domain != "" {
return domain
}
if domain := strings.TrimSpace(d.DownloadDomain); domain != "" && domain != "original" {
return resolveDomainOption(domain)
}
return ""
}

// apiURL builds an api-drive.<domain> URL for the given path (path starts with "/").
func (d *PikPak) apiURL(path string) string {
return "https://api-drive." + d.getApiDomain() + path
}

// userURL builds a user.<domain> URL for the given path (path starts with "/").
func (d *PikPak) userURL(path string) string {
return "https://user." + d.getApiDomain() + path
}

// rewriteDownloadURL rewrites the host of a download/play link to the configured
// download_domain, keeping the original subdomain prefix (e.g. dl-a.mypikpak.com
// -> dl-a.mypikpak.net). It is a no-op when download_domain is empty or the link
// host is not a known PikPak domain.
func (d *PikPak) rewriteDownloadURL(rawURL string) string {
domain := d.getDownloadDomain()
if domain == "" || rawURL == "" {
return rawURL
}
u, err := url.Parse(rawURL)
if err != nil || u.Host == "" {
return rawURL
}
for _, base := range pikPakDomains {
if u.Host == base {
u.Host = domain
return u.String()
}
if strings.HasSuffix(u.Host, "."+base) {
u.Host = strings.TrimSuffix(u.Host, base) + domain
return u.String()
}
}
return rawURL
}

func (d *PikPak) login() error {
// 检查用户名和密码是否为空
if d.Addition.Username == "" || d.Addition.Password == "" {
return errors.New("username or password is empty")
}

url := "https://user.mypikpak.net/v1/auth/signin"
url := d.userURL("/v1/auth/signin")
// 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)
if d.GetCaptchaToken() == "" {
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
Expand Down Expand Up @@ -129,7 +218,7 @@ func (d *PikPak) login() error {
}

func (d *PikPak) refreshToken(refreshToken string) error {
url := "https://user.mypikpak.net/v1/auth/token"
url := d.userURL("/v1/auth/token")
var e ErrResp
res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).
SetHeader("user-agent", "").SetBody(base.Json{
Expand Down Expand Up @@ -229,7 +318,7 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
"page_token": pageToken,
}
var resp Files
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/files", http.MethodGet, func(req *resty.Request) {
_, err := d.request(d.apiURL("/drive/v1/files"), http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp)
if err != nil {
Expand Down Expand Up @@ -394,7 +483,7 @@ func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) err
}
var e ErrResp
var resp CaptchaTokenResponse
_, err := d.request("https://user.mypikpak.net/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
_, err := d.request(d.userURL("/v1/shield/captcha/init"), http.MethodPost, func(req *resty.Request) {
req.SetError(&e).SetBody(param).SetQueryParam("client_id", d.ClientID)
}, &resp)

Expand Down
57 changes: 57 additions & 0 deletions drivers/pikpak/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pikpak

import "testing"

func TestRewriteDownloadURL(t *testing.T) {
cases := []struct {
name string
downloadDomain string // selected download_domain
customDomain string // custom_download_domain (overrides selection)
in string
want string
}{
{"original is no-op", "original", "", "https://dl-a.mypikpak.com/x?sig=1", "https://dl-a.mypikpak.com/x?sig=1"},
{"empty is no-op", "", "", "https://dl-a.mypikpak.com/x?sig=1", "https://dl-a.mypikpak.com/x?sig=1"},
{"option key swaps com to net keeps subdomain and query", "mypikpak_net", "", "https://dl-a10b.mypikpak.com/download/f?X-Amz-Signature=abc", "https://dl-a10b.mypikpak.net/download/f?X-Amz-Signature=abc"},
{"option key swaps net to com", "mypikpak_com", "", "https://vip-lixian-07.mypikpak.net/y", "https://vip-lixian-07.mypikpak.com/y"},
{"option key swaps to pikpak.me", "pikpak_me", "", "https://dl-a.mypikpak.com/z", "https://dl-a.pikpak.me/z"},
{"raw domain value also works", "mypikpak.net", "", "https://dl-a.mypikpak.com/z", "https://dl-a.mypikpak.net/z"},
{"custom overrides selection", "mypikpak_net", "dl.example.org", "https://dl-a.mypikpak.com/z", "https://dl-a.dl.example.org/z"},
{"custom overrides original", "original", "pikpak.me", "https://dl-a.mypikpak.com/z", "https://dl-a.pikpak.me/z"},
{"bare host without subdomain", "mypikpak_net", "", "https://mypikpak.com/z", "https://mypikpak.net/z"},
{"unknown host is left untouched", "mypikpak_net", "", "https://cdn.example.com/z", "https://cdn.example.com/z"},
{"empty url is no-op", "mypikpak_net", "", "", ""},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
d := &PikPak{Addition: Addition{DownloadDomain: c.downloadDomain, CustomDownloadDomain: c.customDomain}}
if got := d.rewriteDownloadURL(c.in); got != c.want {
t.Fatalf("rewriteDownloadURL(%q) sel=%q custom=%q = %q, want %q", c.in, c.downloadDomain, c.customDomain, got, c.want)
}
})
}
}

func TestApiAndUserURL(t *testing.T) {
cases := []struct {
apiDomain string // selected api_domain
customDomain string // custom_api_domain (overrides selection)
wantAPI string
wantUser string
}{
{"", "", "https://api-drive.mypikpak.net/drive/v1/files", "https://user.mypikpak.net/v1/auth/token"},
{"mypikpak_com", "", "https://api-drive.mypikpak.com/drive/v1/files", "https://user.mypikpak.com/v1/auth/token"},
{"pikpak_me", "", "https://api-drive.pikpak.me/drive/v1/files", "https://user.pikpak.me/v1/auth/token"},
{"mypikpak.com", "", "https://api-drive.mypikpak.com/drive/v1/files", "https://user.mypikpak.com/v1/auth/token"},
{"mypikpak_net", "api.example.org", "https://api-drive.api.example.org/drive/v1/files", "https://user.api.example.org/v1/auth/token"},
}
for _, c := range cases {
d := &PikPak{Addition: Addition{ApiDomain: c.apiDomain, CustomApiDomain: c.customDomain}}
if got := d.apiURL("/drive/v1/files"); got != c.wantAPI {
t.Fatalf("apiURL sel=%q custom=%q = %q, want %q", c.apiDomain, c.customDomain, got, c.wantAPI)
}
if got := d.userURL("/v1/auth/token"); got != c.wantUser {
t.Fatalf("userURL sel=%q custom=%q = %q, want %q", c.apiDomain, c.customDomain, got, c.wantUser)
}
}
}
Loading