Repository: AlistGo/alist (language: go)

Issue:
为蓝奏云添加账号密码登录
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

添加用账号密码登陆的方式
因为我不会go语言，所以只能提个issue，期待各位大佬帮我实现
使用cookie登录，会存在过期的情况，而用账号密码登录，可以解决这个问题


### Suggested solution / 实现思路

向https://up.woozooo.com/mlogin.php
post以下数据 
“task=3&uid=” ＋ 用户名 ＋ “&pwd=” ＋ 密码 ＋ “&setSessionId=&setSig=&setScene=&setTocen=&formhash=”

所返回的cookie即为登录所需cookie
期待各位大佬帮我在alist中实现它



### Additional context / 附件

_No response_


Discussion / hints:
Thanks for opening your first issue here! Be sure to follow the issue template!

对了，这种方式无需验证码，我也不知道为什么，但就是可以
是在翻易语言的代码时发现的
![image](https://github.com/alist-org/alist/assets/86878459/6250b317-752f-4d34-97db-1499a7f33a6f)
以上是在易语言中实现的代码的截图，希望对大佬有帮助

这里的UA是这个
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
referer: https://www.lanzouu.com/

Fix patch:
diff --git a/drivers/lanzou/driver.go b/drivers/lanzou/driver.go
index c2e60f5ed8c..cdb56f79658 100644
--- a/drivers/lanzou/driver.go
+++ b/drivers/lanzou/driver.go
@@ -2,9 +2,7 @@ package lanzou
 
 import (
 	"context"
-	"fmt"
 	"net/http"
-	"regexp"
 
 	"github.com/alist-org/alist/v3/drivers/base"
 	"github.com/alist-org/alist/v3/internal/driver"
@@ -19,6 +17,8 @@ type LanZou struct {
 	model.Storage
 	uid string
 	vei string
+
+	flag int32
 }
 
 func (d *LanZou) Config() driver.Config {
@@ -30,16 +30,18 @@ func (d *LanZou) GetAddition() driver.Additional {
 }
 
 func (d *LanZou) Init(ctx context.Context) (err error) {
-	if d.IsCookie() {
+	switch d.Type {
+	case "account":
+		_, err := d.Login()
+		if err != nil {
+			return err
+		}
+		fallthrough
+	case "cookie":
 		if d.RootFolderID == "" {
 			d.RootFolderID = "-1"
 		}
-		ylogin := regexp.MustCompile("ylogin=(.*?);").FindStringSubmatch(d.Cookie)
-		if len(ylogin) < 2 {
-			return fmt.Errorf("cookie does not contain ylogin")
-		}
-		d.uid = ylogin[1]
-		d.vei, err = d.getVei()
+		d.vei, d.uid, err = d.getVeiAndUid()
 	}
 	return
 }
@@ -51,7 +53,7 @@ func (d *LanZou) Drop(ctx context.Context) error {
 
 // 获取的大小和时间不准确
 func (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		return d.GetAllFiles(dir.GetID())
 	} else {
 		return d.GetFileOrFolderByShareUrl(dir.GetID(), d.SharePassword)
@@ -119,7 +121,7 @@ func (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
 }
 
 func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		data, err := d.doupload(func(req *resty.Request) {
 			req.SetContext(ctx)
 			req.SetFormData(map[string]string{
@@ -137,11 +139,11 @@ func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName strin
 			FolID: utils.Json.Get(data, "text").ToString(),
 		}, nil
 	}
-	return nil, errs.NotImplement
+	return nil, errs.NotSupport
 }
 
 func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		if !srcObj.IsDir() {
 			_, err := d.doupload(func(req *resty.Request) {
 				req.SetContext(ctx)
@@ -157,11 +159,11 @@ func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj,
 			return srcObj, nil
 		}
 	}
-	return nil, errs.NotImplement
+	return nil, errs.NotSupport
 }
 
 func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		if !srcObj.IsDir() {
 			_, err := d.doupload(func(req *resty.Request) {
 				req.SetContext(ctx)
@@ -179,11 +181,11 @@ func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) (
 			return srcObj, nil
 		}
 	}
-	return nil, errs.NotImplement
+	return nil, errs.NotSupport
 }
 
 func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		_, err := d.doupload(func(req *resty.Request) {
 			req.SetContext(ctx)
 			if obj.IsDir() {
@@ -200,13 +202,13 @@ func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
 		}, nil)
 		return err
 	}
-	return errs.NotImplement
+	return errs.NotSupport
 }
 
 func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
-	if d.IsCookie() {
+	if d.IsCookie() || d.IsAccount() {
 		var resp RespText[[]FileOrFolder]
-		_, err := d._post(d.BaseUrl+"/fileup.php", func(req *resty.Request) {
+		_, err := d._post(d.BaseUrl+"/html5up.php", func(req *resty.Request) {
 			req.SetFormData(map[string]string{
 				"task":           "1",
 				"vie":            "2",
@@ -221,5 +223,5 @@ func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
 		}
 		return &resp.Text[0], nil
 	}
-	return nil, errs.NotImplement
+	return nil, errs.NotSupport
 }
diff --git a/drivers/lanzou/help.go b/drivers/lanzou/help.go
index ae9b0ac23d0..89f8e0711c3 100644
--- a/drivers/lanzou/help.go
+++ b/drivers/lanzou/help.go
@@ -3,6 +3,7 @@ package lanzou
 import (
 	"bytes"
 	"fmt"
+	"net/http"
 	"regexp"
 	"strconv"
 	"strings"
@@ -190,3 +191,14 @@ func GetExpirationTime(url string) (etime time.Duration) {
 	etime = time.Duration(timestamp-time.Now().Unix()) * time.Second
 	return
 }
+
+func CookieToString(cookies []*http.Cookie) string {
+	if cookies == nil {
+		return ""
+	}
+	cookieStrings := make([]string, len(cookies))
+	for i, cookie := range cookies {
+		cookieStrings[i] = cookie.Name + "=" + cookie.Value
+	}
+	return strings.Join(cookieStrings, ";")
+}
diff --git a/drivers/lanzou/meta.go b/drivers/lanzou/meta.go
index 5fe21d26ac8..c8db6448476 100644
--- a/drivers/lanzou/meta.go
+++ b/drivers/lanzou/meta.go
@@ -6,8 +6,13 @@ import (
 )
 
 type Addition struct {
-	Type   string `json:"type" type:"select" options:"cookie,url" default:"cookie"`
-	Cookie string `json:"cookie" required:"true" help:"about 15 days valid, ignore if shareUrl is used"`
+	Type string `json:"type" type:"select" options:"account,cookie,url" default:"cookie"`
+
+	Account  string `json:"account"`
+	Password string `json:"password"`
+
+	Cookie string `json:"cookie" help:"about 15 days valid, ignore if shareUrl is used"`
+
 	driver.RootID
 	SharePassword  string `json:"share_password"`
 	BaseUrl        string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com" help:"basic URL for file operation"`
@@ -19,6 +24,10 @@ func (a *Addition) IsCookie() bool {
 	return a.Type == "cookie"
 }
 
+func (a *Addition) IsAccount() bool {
+	return a.Type == "account"
+}
+
 var config = driver.Config{
 	Name:        "Lanzou",
 	LocalSort:   true,
diff --git a/drivers/lanzou/types.go b/drivers/lanzou/types.go
index 8be8dd3402d..2e2daf461a5 100644
--- a/drivers/lanzou/types.go
+++ b/drivers/lanzou/types.go
@@ -8,6 +8,7 @@ import (
 
 var ErrFileShareCancel = errors.New("file sharing cancellation")
 var ErrFileNotExist = errors.New("file does not exist")
+var ErrCookieExpiration = errors.New("cookie expiration")
 
 type RespText[T any] struct {
 	Text T `json:"text"`
diff --git a/drivers/lanzou/util.go b/drivers/lanzou/util.go
index 1fb1752147d..6e2f05cc56f 100644
--- a/drivers/lanzou/util.go
+++ b/drivers/lanzou/util.go
@@ -5,13 +5,16 @@ import (
 	"fmt"
 	"net/http"
 	"regexp"
+	"runtime"
 	"strconv"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
 
 	"github.com/alist-org/alist/v3/drivers/base"
 	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/op"
 	"github.com/alist-org/alist/v3/pkg/utils"
 	"github.com/go-resty/resty/v2"
 	log "github.com/sirupsen/logrus"
@@ -37,7 +40,24 @@ func (d *LanZou) get(url string, callback base.ReqCallback) ([]byte, error) {
 }
 
 func (d *LanZou) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
-	return d._post(url, callback, resp, false)
+	data, err := d._post(url, callback, resp, false)
+	if err == ErrCookieExpiration && d.IsAccount() {
+		if atomic.CompareAndSwapInt32(&d.flag, 0, 1) {
+			_, err2 := d.Login()
+			atomic.SwapInt32(&d.flag, 0)
+			if err2 != nil {
+				err = errors.Join(err, err2)
+				d.Status = err.Error()
+				op.MustSaveDriverStorage(d)
+				return data, err
+			}
+		}
+		for atomic.LoadInt32(&d.flag) != 0 {
+			runtime.Gosched()
+		}
+		return d._post(url, callback, resp, false)
+	}
+	return data, err
 }
 
 func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) {
@@ -49,10 +69,12 @@ func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{},
 			}
 			return false
 		})
-		callback(req)
+		if callback != nil {
+			callback(req)
+		}
 	}, up)
 	if err != nil {
-		return nil, err
+		return data, err
 	}
 	switch utils.Json.Get(data, "zt").ToInt() {
 	case 1, 2, 4:
@@ -61,12 +83,14 @@ func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{},
 			utils.Json.Unmarshal(data, resp)
 		}
 		return data, nil
+	case 9: // 登录过期
+		return data, ErrCookieExpiration
 	default:
 		info := utils.Json.Get(data, "inf").ToString()
 		if info == "" {
 			info = utils.Json.Get(data, "info").ToString()
 		}
-		return nil, fmt.Errorf(info)
+		return data, fmt.Errorf(info)
 	}
 }
 
@@ -101,6 +125,28 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
 	return res.Body(), err
 }
 
+func (d *LanZou) Login() ([]*http.Cookie, error) {
+	resp, err := base.NewRestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()).
+		R().SetFormData(map[string]string{
+		"task":         "3",
+		"uid":          d.Account,
+		"pwd":          d.Password,
+		"setSessionId": "",
+		"setSig":       "",
+		"setScene":     "",
+		"setTocen":     "",
+		"formhash":     "",
+	}).Post("https://up.woozooo.com/mlogin.php")
+	if err != nil {
+		return nil, err
+	}
+	if utils.Json.Get(resp.Body(), "zt").ToInt() != 1 {
+		return nil, fmt.Errorf("login err: %s", resp.Body())
+	}
+	d.Cookie = CookieToString(resp.Cookies())
+	return resp.Cookies(), nil
+}
+
 /*
 通过cookie获取数据
 */
@@ -451,21 +497,32 @@ func (d *LanZou) getFileRealInfo(downURL string) (*int64, *time.Time) {
 	return &size, &time
 }
 
-func (d *LanZou) getVei() (string, error) {
-	resp, err := d.get("https://pc.woozooo.com/mydisk.php", func(req *resty.Request) {
+func (d *LanZou) getVeiAndUid() (vei string, uid string, err error) {
+	var resp []byte
+	resp, err = d.get("https://pc.woozooo.com/mydisk.php", func(req *resty.Request) {
 		req.SetQueryParams(map[string]string{
 			"item":   "files",
 			"action": "index",
-			"u":      d.uid,
 		})
 	})
 	if err != nil {
-		return "", err
+		return
+	}
+	// uid
+	uids := regexp.MustCompile(`uid=([^'"&;]+)`).FindStringSubmatch(string(resp))
+	if len(uids) < 2 {
+		err = fmt.Errorf("uid variable not find")
+		return
 	}
+	uid = uids[1]
+
+	// vei
 	html := RemoveNotes(string(resp))
 	data, err := htmlJsonToMap(html)
 	if err != nil {
-		return "", err
+		return
 	}
-	return data["vei"], nil
+	vei = data["vei"]
+
+	return
 }


Test patch: