Feat: alist vendor

pull/40/head
zijiren233 3 years ago
parent d0eea86cd3
commit b4cb16de4e

@ -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

@ -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=

@ -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"`
}

@ -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{

@ -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"`
}

@ -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
}

@ -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")
}

@ -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 {

@ -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)
}

@ -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

@ -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
}

@ -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)
}
}
}

@ -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:

@ -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)
}

@ -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))
}

@ -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,

@ -1,4 +1,4 @@
package Vbilibili
package vendorBilibili
import (
"errors"

@ -1,4 +1,4 @@
package Vbilibili
package vendorBilibili
import (
"net/http"

@ -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))
}
Loading…
Cancel
Save