From b4cb16de4ea8a10d839a9ae985c701c3ca062951 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Mon, 4 Dec 2023 23:21:10 +0800 Subject: [PATCH] Feat: alist vendor --- go.mod | 2 +- go.sum | 2 + internal/conf/vendor.go | 13 +- internal/db/vendor.go | 18 + internal/model/movie.go | 16 +- internal/model/vendor.go | 6 +- internal/op/movie.go | 3 + internal/op/user.go | 39 ++- internal/vendor/alist.go | 311 ++++++++++++++++++ internal/vendor/bilibili.go | 100 +++--- internal/vendor/vendor.go | 3 + server/handlers/init.go | 30 +- server/handlers/movie.go | 114 +++++-- server/handlers/vendors/vendorAlist/login.go | 68 ++++ server/handlers/vendors/vendorAlist/parse.go | 67 ++++ .../{bilibili => vendorBilibili}/bilibili.go | 9 +- .../{bilibili => vendorBilibili}/login.go | 2 +- .../{bilibili => vendorBilibili}/me.go | 2 +- server/handlers/vendors/vendors.go | 25 ++ 19 files changed, 711 insertions(+), 119 deletions(-) create mode 100644 internal/vendor/alist.go create mode 100644 server/handlers/vendors/vendorAlist/login.go create mode 100644 server/handlers/vendors/vendorAlist/parse.go rename server/handlers/vendors/{bilibili => vendorBilibili}/bilibili.go (93%) rename server/handlers/vendors/{bilibili => vendorBilibili}/login.go (99%) rename server/handlers/vendors/{bilibili => vendorBilibili}/me.go (97%) create mode 100644 server/handlers/vendors/vendors.go diff --git a/go.mod b/go.mod index e510975..ecc9dd8 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/soheilhy/cmux v0.1.5 github.com/spf13/cobra v1.8.0 - github.com/synctv-org/vendors v0.1.1-0.20231203064919-c8eeb877a780 + github.com/synctv-org/vendors v0.1.1-0.20231204112116-1882fc1116bf github.com/ulule/limiter/v3 v3.11.2 github.com/zencoder/go-dash/v3 v3.0.3 github.com/zijiren233/gencontainer v0.0.0-20231108115721-8d4bb333a5bc diff --git a/go.sum b/go.sum index 5ed0bf1..7bc2094 100644 --- a/go.sum +++ b/go.sum @@ -354,6 +354,8 @@ github.com/synctv-org/vendors v0.1.1-0.20231203062458-863dbc08c39d h1:FLjsoPTIUj github.com/synctv-org/vendors v0.1.1-0.20231203062458-863dbc08c39d/go.mod h1:DQHEXvrsnX9Ke6B0KVJ+w8uMgM/DZKNkvi6cJmRn+zk= github.com/synctv-org/vendors v0.1.1-0.20231203064919-c8eeb877a780 h1:59T2DtbXn8qkW986k+spwTlQlx8Wl1hmD2zZDYWHv5s= github.com/synctv-org/vendors v0.1.1-0.20231203064919-c8eeb877a780/go.mod h1:DQHEXvrsnX9Ke6B0KVJ+w8uMgM/DZKNkvi6cJmRn+zk= +github.com/synctv-org/vendors v0.1.1-0.20231204112116-1882fc1116bf h1:E5Mu7mqdWCzjy+kSQBvjvmETfhiLXiTuEhfkGhgBRJk= +github.com/synctv-org/vendors v0.1.1-0.20231204112116-1882fc1116bf/go.mod h1:DQHEXvrsnX9Ke6B0KVJ+w8uMgM/DZKNkvi6cJmRn+zk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= diff --git a/internal/conf/vendor.go b/internal/conf/vendor.go index e749457..9c4eb53 100644 --- a/internal/conf/vendor.go +++ b/internal/conf/vendor.go @@ -1,7 +1,8 @@ package conf type VendorConfig struct { - Bilibili map[string]VendorBilibili `yaml:"bilibili" hc:"default use local vendor"` + Bilibili map[string]BilibiliConfig `yaml:"bilibili" hc:"default use local vendor"` + Alist map[string]AlistConfig `yaml:"alist" hc:"default use local vendor"` } func DefaultVendorConfig() VendorConfig { @@ -25,7 +26,7 @@ type Etcd struct { Password string `yaml:"password,omitempty"` } -type VendorBilibili struct { +type VendorBase struct { ServerName string `yaml:"server_name" hc:"if use tls and grpc, servername must set the cert server name" env:"BILIBILI_SERVER_NAME"` Endpoint string `yaml:"endpoint" env:"BILIBILI_ENDPOINT"` JwtSecret string `yaml:"jwt_secret" env:"BILIBILI_JWT_SECRET"` @@ -37,3 +38,11 @@ type VendorBilibili struct { Consul Consul `yaml:"consul,omitempty" hc:"if use consul, must set the endpoint"` Etcd Etcd `yaml:"etcd,omitempty" hc:"if use etcd, must set the endpoints"` } + +type BilibiliConfig struct { + VendorBase `yaml:",inline"` +} + +type AlistConfig struct { + VendorBase `yaml:",inline"` +} diff --git a/internal/db/vendor.go b/internal/db/vendor.go index 6759bcb..0287c86 100644 --- a/internal/db/vendor.go +++ b/internal/db/vendor.go @@ -29,6 +29,24 @@ func WithCookie(cookie []*http.Cookie) CreateVendorConfig { } } +func WithAuthorization(authorization string) CreateVendorConfig { + return func(vendor *model.StreamingVendorInfo) { + vendor.Authorization = authorization + } +} + +func WithPassword(password string) CreateVendorConfig { + return func(vendor *model.StreamingVendorInfo) { + vendor.Password = password + } +} + +func WithHost(host string) CreateVendorConfig { + return func(vendor *model.StreamingVendorInfo) { + vendor.Host = host + } +} + func FirstOrCreateVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor, conf ...CreateVendorConfig) (*model.StreamingVendorInfo, error) { var vendorInfo model.StreamingVendorInfo v := &model.StreamingVendorInfo{ diff --git a/internal/model/movie.go b/internal/model/movie.go index 8f008ae..03de58e 100644 --- a/internal/model/movie.go +++ b/internal/model/movie.go @@ -38,16 +38,17 @@ type BaseMovie struct { type VendorInfo struct { Vendor StreamingVendor `json:"vendor"` + Backend string `json:"backend"` Shared bool `gorm:"not null;default:false" json:"shared"` Bilibili *BilibiliVendorInfo `gorm:"embedded;embeddedPrefix:bilibili_" json:"bilibili,omitempty"` + Alist *AlistVendorInfo `gorm:"embedded;embeddedPrefix:alist_" json:"alist,omitempty"` } type BilibiliVendorInfo struct { - Bvid string `json:"bvid,omitempty"` - Cid uint64 `json:"cid,omitempty"` - Epid uint64 `json:"epid,omitempty"` - Quality uint64 `json:"quality,omitempty"` - VendorName string `json:"vendorName,omitempty"` + Bvid string `json:"bvid,omitempty"` + Cid uint64 `json:"cid,omitempty"` + Epid uint64 `json:"epid,omitempty"` + Quality uint64 `json:"quality,omitempty"` } func (b *BilibiliVendorInfo) Validate() error { @@ -65,3 +66,8 @@ func (b *BilibiliVendorInfo) Validate() error { return nil } + +type AlistVendorInfo struct { + Path string `json:"path,omitempty"` + Password string `json:"password,omitempty"` +} diff --git a/internal/model/vendor.go b/internal/model/vendor.go index ff7ce68..4aa3d7b 100644 --- a/internal/model/vendor.go +++ b/internal/model/vendor.go @@ -9,6 +9,7 @@ type StreamingVendor string const ( StreamingVendorBilibili StreamingVendor = "bilibili" + StreamingVendorAlist StreamingVendor = "alist" ) type StreamingVendorInfo struct { @@ -17,8 +18,11 @@ type StreamingVendorInfo struct { CreatedAt time.Time UpdatedAt time.Time VendorToken + Host string } type VendorToken struct { - Cookies []*http.Cookie `gorm:"serializer:fastjson"` + Cookies []*http.Cookie `gorm:"serializer:fastjson"` + Authorization string + Password string } diff --git a/internal/op/movie.go b/internal/op/movie.go index 10b999b..feb648f 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -240,6 +240,9 @@ func (movie *Movie) validateVendorMovie() error { movie.Movie.Base.Headers["Referer"] = "https://www.bilibili.com" movie.Movie.Base.Headers["User-Agent"] = utils.UA } + + case model.StreamingVendorAlist: + default: return fmt.Errorf("vendor not support") } diff --git a/internal/op/user.go b/internal/op/user.go index a373b91..96cf697 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -64,33 +64,58 @@ func (u *User) CreateRoom(name, password string, conf ...db.CreateRoomConfig) (* return CreateRoom(name, password, maxCount, append(conf, db.WithCreator(&u.User))...) } -func (u *User) NewMovie(movie *model.BaseMovie) *model.Movie { +func (u *User) NewMovie(movie *model.BaseMovie) (*model.Movie, error) { + if movie == nil { + return nil, errors.New("movie is nil") + } + switch movie.VendorInfo.Vendor { + case model.StreamingVendorBilibili: + if movie.VendorInfo.Bilibili == nil { + return nil, errors.New("bilibili payload is nil") + } + case model.StreamingVendorAlist: + if movie.VendorInfo.Alist == nil { + return nil, errors.New("alist payload is nil") + } + } return &model.Movie{ Base: *movie, CreatorID: u.ID, - } + }, nil } func (u *User) AddMovieToRoom(room *Room, movie *model.BaseMovie) error { if !u.HasRoomPermission(room, model.PermissionCreateMovie) { return model.ErrNoPermission } - return room.AddMovie(u.NewMovie(movie)) + m, err := u.NewMovie(movie) + if err != nil { + return err + } + return room.AddMovie(m) } -func (u *User) NewMovies(movies []*model.BaseMovie) []*model.Movie { +func (u *User) NewMovies(movies []*model.BaseMovie) ([]*model.Movie, error) { var ms = make([]*model.Movie, len(movies)) for i, m := range movies { - ms[i] = u.NewMovie(m) + movie, err := u.NewMovie(m) + if err != nil { + return nil, err + } + ms[i] = movie } - return ms + return ms, nil } func (u *User) AddMoviesToRoom(room *Room, movies []*model.BaseMovie) error { if !u.HasRoomPermission(room, model.PermissionCreateMovie) { return model.ErrNoPermission } - return room.AddMovies(u.NewMovies(movies)) + m, err := u.NewMovies(movies) + if err != nil { + return err + } + return room.AddMovies(m) } func (u *User) IsRoot() bool { diff --git a/internal/vendor/alist.go b/internal/vendor/alist.go new file mode 100644 index 0000000..4a0b0f5 --- /dev/null +++ b/internal/vendor/alist.go @@ -0,0 +1,311 @@ +package vendor + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "os" + "time" + + "github.com/go-kratos/aegis/circuitbreaker" + "github.com/go-kratos/aegis/circuitbreaker/sre" + consul "github.com/go-kratos/kratos/contrib/registry/consul/v2" + "github.com/go-kratos/kratos/contrib/registry/etcd/v2" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/middleware/auth/jwt" + kcircuitbreaker "github.com/go-kratos/kratos/v2/middleware/circuitbreaker" + "google.golang.org/grpc" + + ggrpc "github.com/go-kratos/kratos/v2/transport/grpc" + "github.com/go-kratos/kratos/v2/transport/http" + jwtv4 "github.com/golang-jwt/jwt/v4" + "github.com/hashicorp/consul/api" + log "github.com/sirupsen/logrus" + "github.com/synctv-org/synctv/internal/conf" + "github.com/synctv-org/vendors/api/alist" + alistService "github.com/synctv-org/vendors/service/alist" + clientv3 "go.etcd.io/etcd/client/v3" +) + +type AlistInterface = alist.AlistHTTPServer + +func AlistClient(name string) AlistInterface { + if name != "" { + if cli, ok := alistClients[name]; ok { + return cli + } + } + return alistDefaultClient +} + +func AlistClients() map[string]AlistInterface { + return alistClients +} + +var ( + alistClients map[string]AlistInterface + alistDefaultClient AlistInterface +) + +func InitAlistVendors(conf map[string]conf.AlistConfig) error { + if alistClients == nil { + alistClients = make(map[string]AlistInterface, len(conf)) + } + for k, vb := range conf { + cli, err := InitAlist(&vb) + if err != nil { + return err + } + if k == "" { + alistDefaultClient = cli + } else { + alistClients[k] = cli + } + } + if alistDefaultClient == nil { + alistDefaultClient = alistService.NewAlistService(nil) + } + return nil +} + +func InitAlist(conf *conf.AlistConfig) (AlistInterface, error) { + middlewares := []middleware.Middleware{kcircuitbreaker.Client(kcircuitbreaker.WithCircuitBreaker(func() circuitbreaker.CircuitBreaker { + return sre.NewBreaker( + sre.WithRequest(25), + sre.WithWindow(time.Second*15), + ) + }))} + + if conf.JwtSecret != "" { + key := []byte(conf.JwtSecret) + middlewares = append(middlewares, jwt.Client(func(token *jwtv4.Token) (interface{}, error) { + return key, nil + }, jwt.WithSigningMethod(jwtv4.SigningMethodHS256))) + } + + switch conf.Scheme { + case "grpc": + opts := []ggrpc.ClientOption{} + + opts = append(opts, ggrpc.WithMiddleware(middlewares...)) + + if conf.TimeOut != "" { + timeout, err := time.ParseDuration(conf.TimeOut) + if err != nil { + return nil, err + } + opts = append(opts, ggrpc.WithTimeout(timeout)) + } + + if conf.Endpoint != "" { + opts = append(opts, ggrpc.WithEndpoint(conf.Endpoint)) + log.Infof("alist client init success with endpoint: %s", conf.Endpoint) + } else if conf.Consul.Endpoint != "" { + if conf.ServerName == "" { + return nil, errors.New("alist server name is empty") + } + c := api.DefaultConfig() + c.Address = conf.Consul.Endpoint + client, err := api.NewClient(c) + if err != nil { + return nil, err + } + endpoint := fmt.Sprintf("discovery:///%s", conf.ServerName) + dis := consul.New(client) + opts = append(opts, ggrpc.WithEndpoint(endpoint), ggrpc.WithDiscovery(dis)) + log.Infof("alist client init success with consul: %s", conf.Consul.Endpoint) + } else if len(conf.Etcd.Endpoints) > 0 { + if conf.ServerName == "" { + return nil, errors.New("alist server name is empty") + } + endpoint := fmt.Sprintf("discovery:///%s", conf.ServerName) + cli, err := clientv3.New(clientv3.Config{ + Endpoints: conf.Etcd.Endpoints, + Username: conf.Etcd.Username, + Password: conf.Etcd.Password, + }) + if err != nil { + return nil, err + } + dis := etcd.New(cli) + opts = append(opts, ggrpc.WithEndpoint(endpoint), ggrpc.WithDiscovery(dis)) + log.Infof("alist client init success with etcd: %v", conf.Etcd.Endpoints) + } else { + return nil, errors.New("alist client init failed, endpoint is empty") + } + var ( + con *grpc.ClientConn + err error + ) + if conf.Tls { + var rootCAs *x509.CertPool + rootCAs, err = x509.SystemCertPool() + if err != nil { + return nil, err + } + if conf.CustomCAFile != "" { + b, err := os.ReadFile(conf.CustomCAFile) + if err != nil { + panic(err) + } + rootCAs.AppendCertsFromPEM(b) + } + opts = append(opts, ggrpc.WithTLSConfig(&tls.Config{ + RootCAs: rootCAs, + })) + + con, err = ggrpc.Dial( + context.Background(), + opts..., + ) + } else { + con, err = ggrpc.DialInsecure( + context.Background(), + opts..., + ) + } + if err != nil { + return nil, err + } + return newGrpcAlist(alist.NewAlistClient(con)), nil + case "http": + opts := []http.ClientOption{} + + opts = append(opts, http.WithMiddleware(middlewares...)) + + if conf.TimeOut != "" { + timeout, err := time.ParseDuration(conf.TimeOut) + if err != nil { + return nil, err + } + opts = append(opts, http.WithTimeout(timeout)) + } + + if conf.Tls { + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + if conf.CustomCAFile != "" { + b, err := os.ReadFile(conf.CustomCAFile) + if err != nil { + panic(err) + } + rootCAs.AppendCertsFromPEM(b) + } + opts = append(opts, http.WithTLSConfig(&tls.Config{ + RootCAs: rootCAs, + })) + } + + if conf.Endpoint != "" { + opts = append(opts, http.WithEndpoint(conf.Endpoint)) + log.Infof("alist client init success with endpoint: %s", conf.Endpoint) + } else if conf.Consul.Endpoint != "" { + if conf.ServerName == "" { + return nil, errors.New("alist server name is empty") + } + c := api.DefaultConfig() + c.Address = conf.Consul.Endpoint + client, err := api.NewClient(c) + if err != nil { + return nil, err + } + c.Token = conf.Consul.Token + c.TokenFile = conf.Consul.TokenFile + c.PathPrefix = conf.Consul.PathPrefix + c.Namespace = conf.Consul.Namespace + c.Partition = conf.Consul.Partition + endpoint := fmt.Sprintf("discovery:///%s", conf.ServerName) + dis := consul.New(client) + opts = append(opts, http.WithEndpoint(endpoint), http.WithDiscovery(dis)) + log.Infof("alist client init success with consul: %s", conf.Consul.Endpoint) + } else if len(conf.Etcd.Endpoints) > 0 { + if conf.ServerName == "" { + return nil, errors.New("alist server name is empty") + } + endpoint := fmt.Sprintf("discovery:///%s", conf.ServerName) + cli, err := clientv3.New(clientv3.Config{ + Endpoints: conf.Etcd.Endpoints, + Username: conf.Etcd.Username, + Password: conf.Etcd.Password, + }) + if err != nil { + return nil, err + } + dis := etcd.New(cli) + opts = append(opts, http.WithEndpoint(endpoint), http.WithDiscovery(dis)) + log.Infof("alist client init success with etcd: %v", conf.Etcd.Endpoints) + } else { + return nil, errors.New("alist client init failed, endpoint is empty") + } + con, err := http.NewClient( + context.Background(), + opts..., + ) + if err != nil { + return nil, err + } + return newHTTPAlist(alist.NewAlistHTTPClient(con)), nil + default: + return nil, errors.New("unknow alist scheme") + } +} + +var _ AlistInterface = (*grpcAlist)(nil) + +type grpcAlist struct { + client alist.AlistClient +} + +func newGrpcAlist(client alist.AlistClient) *grpcAlist { + return &grpcAlist{ + client: client, + } +} + +func (a *grpcAlist) FsGet(ctx context.Context, req *alist.FsGetReq) (*alist.FsGetResp, error) { + return a.client.FsGet(ctx, req) +} + +func (a *grpcAlist) FsList(ctx context.Context, req *alist.FsListReq) (*alist.FsListResp, error) { + return a.client.FsList(ctx, req) +} + +func (a *grpcAlist) FsOther(ctx context.Context, req *alist.FsOtherReq) (*alist.FsOtherResp, error) { + return a.client.FsOther(ctx, req) +} + +func (a *grpcAlist) Login(ctx context.Context, req *alist.LoginReq) (*alist.LoginResp, error) { + return a.client.Login(ctx, req) +} + +var _ AlistInterface = (*httpAlist)(nil) + +type httpAlist struct { + client alist.AlistHTTPClient +} + +func newHTTPAlist(client alist.AlistHTTPClient) *httpAlist { + return &httpAlist{ + client: client, + } +} + +func (a *httpAlist) FsGet(ctx context.Context, req *alist.FsGetReq) (*alist.FsGetResp, error) { + return a.client.FsGet(ctx, req) +} + +func (a *httpAlist) FsList(ctx context.Context, req *alist.FsListReq) (*alist.FsListResp, error) { + return a.client.FsList(ctx, req) +} + +func (a *httpAlist) FsOther(ctx context.Context, req *alist.FsOtherReq) (*alist.FsOtherResp, error) { + return a.client.FsOther(ctx, req) +} + +func (a *httpAlist) Login(ctx context.Context, req *alist.LoginReq) (*alist.LoginResp, error) { + return a.client.Login(ctx, req) +} diff --git a/internal/vendor/bilibili.go b/internal/vendor/bilibili.go index 27b8fb2..db5ffed 100644 --- a/internal/vendor/bilibili.go +++ b/internal/vendor/bilibili.go @@ -13,6 +13,7 @@ import ( "github.com/go-kratos/aegis/circuitbreaker/sre" consul "github.com/go-kratos/kratos/contrib/registry/consul/v2" "github.com/go-kratos/kratos/contrib/registry/etcd/v2" + "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/middleware/auth/jwt" kcircuitbreaker "github.com/go-kratos/kratos/v2/middleware/circuitbreaker" "google.golang.org/grpc" @@ -28,7 +29,9 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" ) -func BilibiliClient(name string) Bilibili { +type BilibiliInterface = bilibili.BilibiliHTTPServer + +func BilibiliClient(name string) BilibiliInterface { if name != "" { if cli, ok := bilibiliClients[name]; ok { return cli @@ -37,35 +40,18 @@ func BilibiliClient(name string) Bilibili { return bilibiliDefaultClient } -func BilibiliClients() map[string]Bilibili { +func BilibiliClients() map[string]BilibiliInterface { return bilibiliClients } var ( - bilibiliClients map[string]Bilibili - bilibiliDefaultClient Bilibili + bilibiliClients map[string]BilibiliInterface + bilibiliDefaultClient BilibiliInterface ) -type Bilibili interface { - NewQRCode(ctx context.Context, in *bilibili.Empty) (*bilibili.NewQRCodeResp, error) - LoginWithQRCode(ctx context.Context, in *bilibili.LoginWithQRCodeReq) (*bilibili.LoginWithQRCodeResp, error) - NewCaptcha(ctx context.Context, in *bilibili.Empty) (*bilibili.NewCaptchaResp, error) - NewSMS(ctx context.Context, in *bilibili.NewSMSReq) (*bilibili.NewSMSResp, error) - LoginWithSMS(ctx context.Context, in *bilibili.LoginWithSMSReq) (*bilibili.LoginWithSMSResp, error) - ParseVideoPage(ctx context.Context, in *bilibili.ParseVideoPageReq) (*bilibili.VideoPageInfo, error) - GetVideoURL(ctx context.Context, in *bilibili.GetVideoURLReq) (*bilibili.VideoURL, error) - GetDashVideoURL(ctx context.Context, in *bilibili.GetDashVideoURLReq) (*bilibili.GetDashVideoURLResp, error) - GetSubtitles(ctx context.Context, in *bilibili.GetSubtitlesReq) (*bilibili.GetSubtitlesResp, error) - ParsePGCPage(ctx context.Context, in *bilibili.ParsePGCPageReq) (*bilibili.VideoPageInfo, error) - GetPGCURL(ctx context.Context, in *bilibili.GetPGCURLReq) (*bilibili.VideoURL, error) - GetDashPGCURL(ctx context.Context, in *bilibili.GetDashPGCURLReq) (*bilibili.GetDashPGCURLResp, error) - UserInfo(ctx context.Context, in *bilibili.UserInfoReq) (*bilibili.UserInfoResp, error) - Match(ctx context.Context, in *bilibili.MatchReq) (*bilibili.MatchResp, error) -} - -func InitBilibiliVendors(conf map[string]conf.VendorBilibili) error { +func InitBilibiliVendors(conf map[string]conf.BilibiliConfig) error { if bilibiliClients == nil { - bilibiliClients = make(map[string]Bilibili, len(conf)) + bilibiliClients = make(map[string]BilibiliInterface, len(conf)) } for k, vb := range conf { cli, err := InitBilibili(&vb) @@ -84,25 +70,26 @@ func InitBilibiliVendors(conf map[string]conf.VendorBilibili) error { return nil } -func InitBilibili(conf *conf.VendorBilibili) (Bilibili, error) { - key := []byte(conf.JwtSecret) +func InitBilibili(conf *conf.BilibiliConfig) (BilibiliInterface, error) { + middlewares := []middleware.Middleware{kcircuitbreaker.Client(kcircuitbreaker.WithCircuitBreaker(func() circuitbreaker.CircuitBreaker { + return sre.NewBreaker( + sre.WithRequest(25), + sre.WithWindow(time.Second*15), + ) + }))} + + if conf.JwtSecret != "" { + key := []byte(conf.JwtSecret) + middlewares = append(middlewares, jwt.Client(func(token *jwtv4.Token) (interface{}, error) { + return key, nil + }, jwt.WithSigningMethod(jwtv4.SigningMethodHS256))) + } + switch conf.Scheme { case "grpc": opts := []ggrpc.ClientOption{} - if conf.JwtSecret != "" { - opts = append(opts, ggrpc.WithMiddleware( - jwt.Client(func(token *jwtv4.Token) (interface{}, error) { - return key, nil - }, jwt.WithSigningMethod(jwtv4.SigningMethodHS256)), - kcircuitbreaker.Client(kcircuitbreaker.WithCircuitBreaker(func() circuitbreaker.CircuitBreaker { - return sre.NewBreaker( - sre.WithRequest(25), - sre.WithWindow(time.Second*15), - ) - })), - )) - } + opts = append(opts, ggrpc.WithMiddleware(middlewares...)) if conf.TimeOut != "" { timeout, err := time.ParseDuration(conf.TimeOut) @@ -185,6 +172,17 @@ func InitBilibili(conf *conf.VendorBilibili) (Bilibili, error) { return newGrpcBilibili(bilibili.NewBilibiliClient(con)), nil case "http": opts := []http.ClientOption{} + + opts = append(opts, http.WithMiddleware(middlewares...)) + + if conf.TimeOut != "" { + timeout, err := time.ParseDuration(conf.TimeOut) + if err != nil { + return nil, err + } + opts = append(opts, http.WithTimeout(timeout)) + } + if conf.Tls { rootCAs, err := x509.SystemCertPool() if err != nil { @@ -202,28 +200,6 @@ func InitBilibili(conf *conf.VendorBilibili) (Bilibili, error) { })) } - if conf.JwtSecret != "" { - opts = append(opts, http.WithMiddleware( - jwt.Client(func(token *jwtv4.Token) (interface{}, error) { - return key, nil - }, jwt.WithSigningMethod(jwtv4.SigningMethodHS256)), - kcircuitbreaker.Client(kcircuitbreaker.WithCircuitBreaker(func() circuitbreaker.CircuitBreaker { - return sre.NewBreaker( - sre.WithRequest(25), - sre.WithWindow(time.Second*15), - ) - })), - )) - } - - if conf.TimeOut != "" { - timeout, err := time.ParseDuration(conf.TimeOut) - if err != nil { - return nil, err - } - opts = append(opts, http.WithTimeout(timeout)) - } - if conf.Endpoint != "" { opts = append(opts, http.WithEndpoint(conf.Endpoint)) log.Infof("bilibili client init success with endpoint: %s", conf.Endpoint) @@ -278,7 +254,7 @@ func InitBilibili(conf *conf.VendorBilibili) (Bilibili, error) { } } -var _ Bilibili = (*grpcBilibili)(nil) +var _ BilibiliInterface = (*grpcBilibili)(nil) type grpcBilibili struct { client bilibili.BilibiliClient @@ -346,7 +322,7 @@ func (g *grpcBilibili) Match(ctx context.Context, in *bilibili.MatchReq) (*bilib return g.client.Match(ctx, in) } -var _ Bilibili = (*httpBilibili)(nil) +var _ BilibiliInterface = (*httpBilibili)(nil) type httpBilibili struct { client bilibili.BilibiliHTTPClient diff --git a/internal/vendor/vendor.go b/internal/vendor/vendor.go index 1e6a8ab..4eb5c79 100644 --- a/internal/vendor/vendor.go +++ b/internal/vendor/vendor.go @@ -14,5 +14,8 @@ func Init(conf *conf.VendorConfig) error { if err := InitBilibiliVendors(conf.Bilibili); err != nil { return err } + if err := InitAlistVendors(conf.Alist); err != nil { + return err + } return nil } diff --git a/server/handlers/init.go b/server/handlers/init.go index 106b074..b0617f9 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -2,7 +2,9 @@ package handlers import ( "github.com/gin-gonic/gin" - Vbilibili "github.com/synctv-org/synctv/server/handlers/vendors/bilibili" + "github.com/synctv-org/synctv/server/handlers/vendors" + "github.com/synctv-org/synctv/server/handlers/vendors/vendorAlist" + "github.com/synctv-org/synctv/server/handlers/vendors/vendorBilibili" "github.com/synctv-org/synctv/server/middlewares" "github.com/synctv-org/synctv/utils" ) @@ -172,28 +174,36 @@ func Init(e *gin.Engine) { { vendor := needAuthUserApi.Group("/vendor") + vendor.GET("/backends/:vendor", vendors.Backends) + { bilibili := vendor.Group("/bilibili") login := bilibili.Group("/login") - login.GET("/qr", Vbilibili.NewQRCode) + login.GET("/qr", vendorBilibili.NewQRCode) + + login.POST("/qr", vendorBilibili.LoginWithQR) - login.POST("/qr", Vbilibili.LoginWithQR) + login.GET("/captcha", vendorBilibili.NewCaptcha) - login.GET("/captcha", Vbilibili.NewCaptcha) + login.POST("/sms/send", vendorBilibili.NewSMS) - login.POST("/sms/send", Vbilibili.NewSMS) + login.POST("/sms/login", vendorBilibili.LoginWithSMS) - login.POST("/sms/login", Vbilibili.LoginWithSMS) + bilibili.POST("/parse", vendorBilibili.Parse) - bilibili.POST("/parse", Vbilibili.Parse) + bilibili.GET("/me", vendorBilibili.Me) - bilibili.GET("/vendors", Vbilibili.Vendors) + bilibili.POST("/logout", vendorBilibili.Logout) + } + + { + alist := vendor.Group("/alist") - bilibili.GET("/me", Vbilibili.Me) + alist.POST("/login", vendorAlist.Login) - bilibili.POST("/logout", Vbilibili.Logout) + alist.POST("/list", vendorAlist.List) } } } diff --git a/server/handlers/movie.go b/server/handlers/movie.go index cda90ec..2e322a8 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -28,6 +28,7 @@ import ( pb "github.com/synctv-org/synctv/proto/message" "github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/utils" + "github.com/synctv-org/vendors/api/alist" "github.com/synctv-org/vendors/api/bilibili" "github.com/zencoder/go-dash/v3/mpd" "github.com/zijiren233/livelib/protocol/hls" @@ -670,19 +671,20 @@ type bilibiliCache struct { urls []string } -func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string, info *dbModel.BilibiliVendorInfo) func() (any, error) { +func initBilibiliMPDCache(ctx context.Context, movie dbModel.Movie) func() (any, error) { return func() (any, error) { - v, err := db.FirstOrInitVendorByUserIDAndVendor(CreatorID, dbModel.StreamingVendorBilibili) + v, err := db.FirstOrInitVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili) if err != nil { return nil, err } - cli := vendor.BilibiliClient(info.VendorName) + cli := vendor.BilibiliClient(movie.Base.VendorInfo.Backend) var m, hevcM *mpd.MPD - if info.Bvid != "" && info.Cid != 0 { + biliInfo := movie.Base.VendorInfo.Bilibili + if biliInfo.Bvid != "" && biliInfo.Cid != 0 { resp, err := cli.GetDashVideoURL(ctx, &bilibili.GetDashVideoURLReq{ Cookies: utils.HttpCookieToMap(v.Cookies), - Bvid: info.Bvid, - Cid: info.Cid, + Bvid: biliInfo.Bvid, + Cid: biliInfo.Cid, }) if err != nil { return nil, err @@ -696,10 +698,10 @@ func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string return nil, err } - } else if info.Epid != 0 { + } else if biliInfo.Epid != 0 { resp, err := cli.GetDashPGCURL(ctx, &bilibili.GetDashPGCURLReq{ Cookies: utils.HttpCookieToMap(v.Cookies), - Epid: info.Epid, + Epid: biliInfo.Epid, }) if err != nil { return nil, err @@ -716,7 +718,7 @@ func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string } else { return nil, errors.New("bvid and epid are empty") } - m.BaseURL = append(m.BaseURL, fmt.Sprintf("/api/movie/proxy/%s/", roomID)) + m.BaseURL = append(m.BaseURL, fmt.Sprintf("/api/movie/proxy/%s/", movie.RoomID)) id := 0 movies := []string{} for _, p := range m.Periods { @@ -724,7 +726,7 @@ func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string for _, r := range as.Representations { for i := range r.BaseURL { movies = append(movies, r.BaseURL[i]) - r.BaseURL[i] = fmt.Sprintf("%s?id=%d", movieID, id) + r.BaseURL[i] = fmt.Sprintf("%s?id=%d", movie.ID, id) id++ } } @@ -735,7 +737,7 @@ func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string for _, r := range as.Representations { for i := range r.BaseURL { movies = append(movies, r.BaseURL[i]) - r.BaseURL[i] = fmt.Sprintf("%s?id=%d&t=hevc", movieID, id) + r.BaseURL[i] = fmt.Sprintf("%s?id=%d&t=hevc", movie.ID, id) id++ } } @@ -757,28 +759,29 @@ func initBilibiliMPDCache(ctx context.Context, roomID, movieID, CreatorID string } } -func initBilibiliShareCache(ctx context.Context, CreatorID string, info *dbModel.BilibiliVendorInfo) func() (any, error) { +func initBilibiliShareCache(ctx context.Context, movie dbModel.Movie) func() (any, error) { return func() (any, error) { - v, err := db.FirstOrInitVendorByUserIDAndVendor(CreatorID, dbModel.StreamingVendorBilibili) + v, err := db.FirstOrInitVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili) if err != nil { return "", err } - cli := vendor.BilibiliClient(info.VendorName) + cli := vendor.BilibiliClient(movie.Base.VendorInfo.Backend) var u string - if info.Bvid != "" { + biliInfo := movie.Base.VendorInfo.Bilibili + if biliInfo.Bvid != "" { resp, err := cli.GetVideoURL(ctx, &bilibili.GetVideoURLReq{ Cookies: utils.HttpCookieToMap(v.Cookies), - Bvid: info.Bvid, - Cid: info.Cid, + Bvid: biliInfo.Bvid, + Cid: biliInfo.Cid, }) if err != nil { return "", err } u = resp.Url - } else if info.Epid != 0 { + } else if biliInfo.Epid != 0 { resp, err := cli.GetPGCURL(ctx, &bilibili.GetPGCURLReq{ Cookies: utils.HttpCookieToMap(v.Cookies), - Epid: info.Epid, + Epid: biliInfo.Epid, }) if err != nil { return "", err @@ -791,6 +794,54 @@ func initBilibiliShareCache(ctx context.Context, CreatorID string, info *dbModel } } +type alistCache struct { + url string +} + +func initAlistCache(ctx context.Context, movie dbModel.Movie) func() (any, error) { + return func() (any, error) { + v, err := db.FirstOrCreateVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorAlist) + if err != nil { + return nil, err + } + if v.Host == "" { + return nil, errors.New("not bind alist vendor") + } + cli := vendor.AlistClient(movie.Base.VendorInfo.Backend) + fg, err := cli.FsGet(ctx, &alist.FsGetReq{ + Host: v.Host, + Token: v.Authorization, + Path: movie.Base.VendorInfo.Alist.Path, + Password: movie.Base.VendorInfo.Alist.Password, + }) + if err != nil { + return nil, err + } + + if fg.IsDir { + return nil, errors.New("path is dir") + } + + cache := &alistCache{ + url: fg.RawUrl, + } + if fg.Provider == "AliyundriveOpen" { + fo, err := cli.FsOther(ctx, &alist.FsOtherReq{ + Host: v.Host, + Token: v.Authorization, + Path: movie.Base.VendorInfo.Alist.Path, + Password: movie.Base.VendorInfo.Alist.Password, + Method: "video_preview", + }) + if err != nil { + return nil, err + } + cache.url = fo.VideoPreviewPlayInfo.LiveTranscodingTaskList[len(fo.VideoPreviewPlayInfo.LiveTranscodingTaskList)-1].Url + } + return cache, nil + } +} + func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { switch movie.Movie.Base.VendorInfo.Vendor { case dbModel.StreamingVendorBilibili: @@ -798,7 +849,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { if t != "hevc" { t = "" } - bvc, err := movie.Cache().InitOrLoadCache(t, initBilibiliMPDCache(ctx, movie.Movie.RoomID, movie.Movie.ID, movie.Movie.CreatorID, movie.Movie.Base.VendorInfo.Bilibili), time.Minute*119) + bvc, err := movie.Cache().InitOrLoadCache(t, initBilibiliMPDCache(ctx, movie.Movie), time.Minute*119) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return @@ -848,12 +899,12 @@ func parse2VendorMovie(ctx context.Context, userID string, movie *op.Movie) (err switch movie.Movie.Base.VendorInfo.Vendor { case dbModel.StreamingVendorBilibili: if !movie.Movie.Base.Proxy { - c, err := movie.Cache().InitOrLoadCache(userID, initBilibiliShareCache(ctx, movie.Movie.CreatorID, movie.Movie.Base.VendorInfo.Bilibili), time.Minute*119) + cache, err := movie.Cache().InitOrLoadCache(userID, initBilibiliShareCache(ctx, movie.Movie), time.Minute*119) if err != nil { return err } - dataI, err := c.Get() + dataI, err := cache.Get() if err != nil { return err } @@ -867,7 +918,26 @@ func parse2VendorMovie(ctx context.Context, userID string, movie *op.Movie) (err } else { movie.Movie.Base.Type = "mpd" } + return nil + + case dbModel.StreamingVendorAlist: + cache, err := movie.Cache().InitOrLoadCache("", initAlistCache(ctx, movie.Movie), time.Minute*15) + if err != nil { + return err + } + + dataI, err := cache.Get() + if err != nil { + return err + } + + data, ok := dataI.(*alistCache) + if !ok { + return errors.New("cache type error") + } + movie.Movie.Base.Url = data.url + movie.Movie.Base.VendorInfo.Alist = nil return nil default: diff --git a/server/handlers/vendors/vendorAlist/login.go b/server/handlers/vendors/vendorAlist/login.go new file mode 100644 index 0000000..9c0f0f8 --- /dev/null +++ b/server/handlers/vendors/vendorAlist/login.go @@ -0,0 +1,68 @@ +package vendorAlist + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/synctv-org/synctv/internal/db" + dbModel "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/internal/vendor" + "github.com/synctv-org/synctv/server/model" + "github.com/synctv-org/vendors/api/alist" +) + +type LoginReq struct { + Host string `json:"host"` + Username string `json:"username"` + Password string `json:"password"` +} + +func (r *LoginReq) Validate() error { + if r.Host == "" { + return errors.New("host is required") + } + return nil +} + +func (r *LoginReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func Login(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := LoginReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if req.Username == "" { + _, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist, db.WithHost(req.Host)) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + } else { + resp, err := vendor.AlistClient("").Login(ctx, &alist.LoginReq{ + Host: req.Host, + Username: req.Username, + Password: req.Password, + }) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + _, err = db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist, db.WithAuthorization(resp.Token), db.WithHost(req.Host)) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/vendors/vendorAlist/parse.go b/server/handlers/vendors/vendorAlist/parse.go new file mode 100644 index 0000000..2f1d0b5 --- /dev/null +++ b/server/handlers/vendors/vendorAlist/parse.go @@ -0,0 +1,67 @@ +package vendorAlist + +import ( + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/synctv-org/synctv/internal/db" + dbModel "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/internal/vendor" + "github.com/synctv-org/synctv/server/model" + "github.com/synctv-org/vendors/api/alist" +) + +type ListReq struct { + Path string `json:"path"` + Password string `json:"password"` +} + +func (r *ListReq) Validate() error { + if r.Path == "" { + r.Password = "/" + } + return nil +} + +func (r *ListReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func List(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := ListReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + v, err := db.FirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + if v.Authorization == "" { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("unauthorized")) + return + } + + var cli = vendor.AlistClient(ctx.Query("backend")) + + resp, err := cli.FsList(ctx, &alist.FsListReq{ + Token: v.Authorization, + Password: req.Password, + Path: req.Path, + Host: v.Host, + Refresh: true, + }) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(resp)) +} diff --git a/server/handlers/vendors/bilibili/bilibili.go b/server/handlers/vendors/vendorBilibili/bilibili.go similarity index 93% rename from server/handlers/vendors/bilibili/bilibili.go rename to server/handlers/vendors/vendorBilibili/bilibili.go index 031ceca..13e98a5 100644 --- a/server/handlers/vendors/bilibili/bilibili.go +++ b/server/handlers/vendors/vendorBilibili/bilibili.go @@ -1,4 +1,4 @@ -package Vbilibili +package vendorBilibili import ( "errors" @@ -14,7 +14,6 @@ import ( "github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/utils" "github.com/synctv-org/vendors/api/bilibili" - "golang.org/x/exp/maps" ) type ParseReq struct { @@ -32,10 +31,6 @@ func (r *ParseReq) Decode(ctx *gin.Context) error { return json.NewDecoder(ctx.Request.Body).Decode(r) } -func Vendors(ctx *gin.Context) { - ctx.JSON(http.StatusOK, model.NewApiDataResp(maps.Keys(vendor.BilibiliClients()))) -} - func Parse(ctx *gin.Context) { user := ctx.MustGet("user").(*op.User) @@ -45,7 +40,7 @@ func Parse(ctx *gin.Context) { return } - var cli = vendor.BilibiliClient(ctx.Query("vendor")) + var cli = vendor.BilibiliClient(ctx.Query("backend")) resp, err := cli.Match(ctx, &bilibili.MatchReq{ Url: req.URL, diff --git a/server/handlers/vendors/bilibili/login.go b/server/handlers/vendors/vendorBilibili/login.go similarity index 99% rename from server/handlers/vendors/bilibili/login.go rename to server/handlers/vendors/vendorBilibili/login.go index 2c49fac..239be5d 100644 --- a/server/handlers/vendors/bilibili/login.go +++ b/server/handlers/vendors/vendorBilibili/login.go @@ -1,4 +1,4 @@ -package Vbilibili +package vendorBilibili import ( "errors" diff --git a/server/handlers/vendors/bilibili/me.go b/server/handlers/vendors/vendorBilibili/me.go similarity index 97% rename from server/handlers/vendors/bilibili/me.go rename to server/handlers/vendors/vendorBilibili/me.go index cf97e3a..6b3b93d 100644 --- a/server/handlers/vendors/bilibili/me.go +++ b/server/handlers/vendors/vendorBilibili/me.go @@ -1,4 +1,4 @@ -package Vbilibili +package vendorBilibili import ( "net/http" diff --git a/server/handlers/vendors/vendors.go b/server/handlers/vendors/vendors.go new file mode 100644 index 0000000..bc42337 --- /dev/null +++ b/server/handlers/vendors/vendors.go @@ -0,0 +1,25 @@ +package vendors + +import ( + "net/http" + + "github.com/gin-gonic/gin" + dbModel "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/vendor" + "github.com/synctv-org/synctv/server/model" + "golang.org/x/exp/maps" +) + +func Backends(ctx *gin.Context) { + var backends []string + switch dbModel.StreamingVendor(ctx.Param("vendor")) { + case dbModel.StreamingVendorBilibili: + backends = maps.Keys(vendor.BilibiliClients()) + case dbModel.StreamingVendorAlist: + backends = maps.Keys(vendor.AlistClients()) + default: + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid vendor")) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(backends)) +}