From d3431d900445ba9d80b557eab4c7dd53617b2bf5 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Wed, 27 Dec 2023 17:52:15 +0800 Subject: [PATCH] Feat: support mutle alist client --- internal/cache/alist.go | 49 ++++++----- internal/cache/emby.go | 25 +++--- internal/db/vendorRecord.go | 27 ++++-- internal/model/movie.go | 18 ++++ internal/model/user.go | 2 +- internal/model/vendorRecord.go | 8 ++ server/handlers/init.go | 2 + server/handlers/vendors/vendorAlist/list.go | 87 +++++++++++++++++--- server/handlers/vendors/vendorAlist/login.go | 29 +++++-- server/handlers/vendors/vendorAlist/me.go | 47 ++++++++++- server/handlers/vendors/vendorEmby/list.go | 17 ++-- server/handlers/vendors/vendorEmby/login.go | 27 +++--- server/handlers/vendors/vendorEmby/me.go | 4 + server/model/vendor.go | 31 +++++-- utils/utils.go | 5 +- 15 files changed, 285 insertions(+), 93 deletions(-) diff --git a/internal/cache/alist.go b/internal/cache/alist.go index 65fb0bf..9b12223 100644 --- a/internal/cache/alist.go +++ b/internal/cache/alist.go @@ -16,33 +16,32 @@ import ( "github.com/zijiren233/gencontainer/refreshcache" ) -type AlistUserCache = refreshcache.RefreshCache[*AlistUserCacheData, struct{}] +type AlistUserCache = MapCache[*AlistUserCacheData, struct{}] type AlistUserCacheData struct { - Host string - Token string - Backend string + Host string + ServerID string + Token string + Backend string } func NewAlistUserCache(userID string) *AlistUserCache { - f := AlistAuthorizationCacheWithUserIDInitFunc(userID) - return refreshcache.NewRefreshCache(func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) { - return f(ctx) + return newMapCache[*AlistUserCacheData, struct{}](func(ctx context.Context, key string, args ...struct{}) (*AlistUserCacheData, error) { + return AlistAuthorizationCacheWithUserIDInitFunc(ctx, userID, key) }, 0) } -func AlistAuthorizationCacheWithUserIDInitFunc(userID string) func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) { - return func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) { - v, err := db.GetAlistVendor(userID) - if err != nil { - return nil, err - } - return AlistAuthorizationCacheWithConfigInitFunc(ctx, v) +func AlistAuthorizationCacheWithUserIDInitFunc(ctx context.Context, userID, serverID string) (*AlistUserCacheData, error) { + v, err := db.GetAlistVendor(userID, serverID) + if err != nil { + return nil, err } + return AlistAuthorizationCacheWithConfigInitFunc(ctx, v) } func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.AlistVendor) (*AlistUserCacheData, error) { cli := vendor.LoadAlistClient(v.Backend) + model.GenAlistServerID(v) if v.Username == "" { _, err := cli.Me(ctx, &alist.MeReq{ @@ -52,8 +51,9 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali return nil, err } return &AlistUserCacheData{ - Host: v.Host, - Backend: v.Backend, + Host: v.Host, + ServerID: v.ServerID, + Backend: v.Backend, }, nil } else { resp, err := cli.Login(ctx, &alist.LoginReq{ @@ -67,9 +67,10 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali } return &AlistUserCacheData{ - Host: v.Host, - Token: resp.Token, - Backend: v.Backend, + Host: v.Host, + ServerID: v.ServerID, + Token: resp.Token, + Backend: v.Backend, }, nil } } @@ -141,7 +142,15 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar if len(args) == 0 { return nil, errors.New("need alist user cache") } - aucd, err := args[0].Get(ctx) + var ( + serverID string + err error + ) + serverID, movie.Base.VendorInfo.Alist.Path, err = model.GetAlistServerIdFromPath(movie.Base.VendorInfo.Alist.Path) + if err != nil { + return nil, err + } + aucd, err := args[0].LoadOrStore(ctx, serverID) if err != nil { return nil, err } diff --git a/internal/cache/emby.go b/internal/cache/emby.go index 15ea829..98eb3e5 100644 --- a/internal/cache/emby.go +++ b/internal/cache/emby.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "net/url" - "strings" "github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/model" @@ -33,15 +32,10 @@ func NewEmbyUserCache(userID string) *EmbyUserCache { } func EmbyAuthorizationCacheWithUserIDInitFunc(userID, serverID string) (*EmbyUserCacheData, error) { - var ( - v *model.EmbyVendor - err error - ) if serverID == "" { - v, err = db.GetEmbyFirstVendor(userID) - } else { - v, err = db.GetEmbyVendor(userID, serverID) + return nil, errors.New("serverID is required") } + v, err := db.GetEmbyVendor(userID, serverID) if err != nil { return nil, err } @@ -85,12 +79,13 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg return nil, errors.New("need emby user cache") } - var serverID, itemID string - if s := strings.Split(movie.Base.VendorInfo.Emby.Path, "/"); len(s) == 2 { - serverID = s[0] - itemID = s[1] - } else { - return nil, errors.New("path is invalid") + var ( + serverID string + err error + ) + serverID, movie.Base.VendorInfo.Emby.Path, err = model.GetEmbyServerIdFromPath(movie.Base.VendorInfo.Emby.Path) + if err != nil { + return nil, err } aucd, err := args[0].LoadOrStore(ctx, serverID) @@ -108,7 +103,7 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg data, err := cli.GetItem(ctx, &emby.GetItemReq{ Host: aucd.Host, Token: aucd.ApiKey, - ItemId: itemID, + ItemId: movie.Base.VendorInfo.Emby.Path, }) if err != nil { return nil, err diff --git a/internal/db/vendorRecord.go b/internal/db/vendorRecord.go index cb2b52e..dfd7248 100644 --- a/internal/db/vendorRecord.go +++ b/internal/db/vendorRecord.go @@ -32,19 +32,32 @@ func DeleteBilibiliVendor(userID string) error { return db.Where("user_id = ?", userID).Delete(&model.BilibiliVendor{}).Error } -func GetAlistVendor(userID string) (*model.AlistVendor, error) { +func GetAlistVendors(userID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.AlistVendor, error) { + var vendors []*model.AlistVendor + err := db.Scopes(scopes...).Where("user_id = ?", userID).Find(&vendors).Error + return vendors, err +} + +func GetAlistVendorsCount(userID string, scopes ...func(*gorm.DB) *gorm.DB) (int64, error) { + var count int64 + err := db.Scopes(scopes...).Where("user_id = ?", userID).Model(&model.AlistVendor{}).Count(&count).Error + return count, err +} + +func GetAlistVendor(userID, serverID string) (*model.AlistVendor, error) { var vendor model.AlistVendor - err := db.Where("user_id = ?", userID).First(&vendor).Error + err := db.Where("user_id = ? AND server_id = ?", userID, serverID).First(&vendor).Error return &vendor, HandleNotFound(err, "vendor") } func CreateOrSaveAlistVendor(vendorInfo *model.AlistVendor) (*model.AlistVendor, error) { - if vendorInfo.UserID == "" { - return nil, errors.New("user_id must not be empty") + if vendorInfo.UserID == "" || vendorInfo.ServerID == "" { + return nil, errors.New("user_id and server_id must not be empty") } return vendorInfo, Transactional(func(tx *gorm.DB) error { if errors.Is(tx.First(&model.AlistVendor{ - UserID: vendorInfo.UserID, + UserID: vendorInfo.UserID, + ServerID: vendorInfo.ServerID, }).Error, gorm.ErrRecordNotFound) { return tx.Create(&vendorInfo).Error } else { @@ -53,8 +66,8 @@ func CreateOrSaveAlistVendor(vendorInfo *model.AlistVendor) (*model.AlistVendor, }) } -func DeleteAlistVendor(userID string) error { - return db.Where("user_id = ?", userID).Delete(&model.AlistVendor{}).Error +func DeleteAlistVendor(userID, serverID string) error { + return db.Where("user_id = ? AND server_id = ?", userID, serverID).Delete(&model.AlistVendor{}).Error } func GetEmbyVendors(userID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.EmbyVendor, error) { diff --git a/internal/model/movie.go b/internal/model/movie.go index 5928029..09298c2 100644 --- a/internal/model/movie.go +++ b/internal/model/movie.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "strings" "time" "github.com/synctv-org/synctv/utils" @@ -85,10 +86,19 @@ func (b *BilibiliStreamingInfo) Validate() error { } type AlistStreamingInfo struct { + // {/}serverId/Path Path string `gorm:"type:varchar(4096)" json:"path,omitempty"` Password string `gorm:"type:varchar(256)" json:"password,omitempty"` } +func GetAlistServerIdFromPath(path string) (serverID string, filePath string, err error) { + before, after, found := strings.Cut(strings.TrimLeft(path, "/"), "/") + if !found { + return "", path, fmt.Errorf("path is invalid") + } + return before, after, nil +} + func (a *AlistStreamingInfo) Validate() error { if a.Path == "" { return fmt.Errorf("path is empty") @@ -123,9 +133,17 @@ func (a *AlistStreamingInfo) AfterFind(tx *gorm.DB) error { } type EmbyStreamingInfo struct { + // {/}serverId/ItemId Path string `gorm:"type:varchar(52)" json:"path,omitempty"` } +func GetEmbyServerIdFromPath(path string) (serverID string, filePath string, err error) { + if s := strings.Split(strings.TrimLeft(path, "/"), "/"); len(s) == 2 { + return s[0], s[1], nil + } + return "", path, fmt.Errorf("path is invalid") +} + func (e *EmbyStreamingInfo) Validate() error { if e.Path == "" { return fmt.Errorf("path is empty") diff --git a/internal/model/user.go b/internal/model/user.go index 64fe61e..786b08e 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -51,7 +51,7 @@ type User struct { Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - AlistVendor *AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } diff --git a/internal/model/vendorRecord.go b/internal/model/vendorRecord.go index 1fab7e1..03d748e 100644 --- a/internal/model/vendorRecord.go +++ b/internal/model/vendorRecord.go @@ -3,6 +3,7 @@ package model import ( "time" + "github.com/google/uuid" "github.com/synctv-org/synctv/utils" "gorm.io/gorm" ) @@ -48,11 +49,18 @@ type AlistVendor struct { UpdatedAt time.Time UserID string `gorm:"primaryKey;type:char(32)"` Backend string `gorm:"type:varchar(64)"` + ServerID string `gorm:"primaryKey;type:char(32)"` Host string `gorm:"not null;type:varchar(256)"` Username string `gorm:"type:varchar(256)"` HashedPassword []byte } +func GenAlistServerID(a *AlistVendor) { + if a.ServerID == "" { + a.ServerID = utils.SortUUIDWithUUID(uuid.NewMD5(uuid.NameSpaceURL, []byte(a.Host))) + } +} + func (a *AlistVendor) BeforeSave(tx *gorm.DB) error { key := utils.GenCryptoKey(a.UserID) var err error diff --git a/server/handlers/init.go b/server/handlers/init.go index 1406991..58ff50a 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -245,6 +245,8 @@ func initVendor(vendor *gin.RouterGroup) { alist.POST("/list", vendorAlist.List) alist.GET("/me", vendorAlist.Me) + + alist.GET("/binds", vendorAlist.Binds) } { diff --git a/server/handlers/vendors/vendorAlist/list.go b/server/handlers/vendors/vendorAlist/list.go index f7d3616..e74b40a 100644 --- a/server/handlers/vendors/vendorAlist/list.go +++ b/server/handlers/vendors/vendorAlist/list.go @@ -9,20 +9,30 @@ import ( "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/synctv/utils" "github.com/synctv-org/vendors/api/alist" + "gorm.io/gorm" ) type ListReq struct { + ServerID string `json:"-"` Path string `json:"path"` Password string `json:"password"` Refresh bool `json:"refresh"` } -func (r *ListReq) Validate() error { +func (r *ListReq) Validate() (err error) { + if r.Path == "" { + return nil + } + r.ServerID, r.Path, err = dbModel.GetAlistServerIdFromPath(r.Path) + if err != nil { + return err + } if r.Path == "" { r.Path = "/" } @@ -50,14 +60,65 @@ func List(ctx *gin.Context) { return } + page, size, err := utils.GetPageAndMax(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if req.ServerID == "" { + socpes := [](func(*gorm.DB) *gorm.DB){ + db.OrderByCreatedAtAsc, + } + ev, err := db.GetAlistVendors(user.ID, append(socpes, db.Paginate(page, size))...) + if err != nil { + if errors.Is(err, db.ErrNotFound("vendor")) { + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found")) + return + } + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + total, err := db.GetAlistVendorsCount(user.ID, socpes...) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + resp := AlistFSListResp{ + Paths: []*model.Path{ + { + Name: "", + Path: "", + }, + }, + Total: uint64(total), + } + + for _, evi := range ev { + resp.Items = append(resp.Items, &AlistFileItem{ + Item: &model.Item{ + Name: evi.Host, + Path: evi.ServerID + `/`, + IsDir: true, + }, + }) + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(resp)) + + return + } + if !strings.HasPrefix(req.Path, "/") { req.Path = "/" + req.Path } - aucd, err := user.AlistCache().Get(ctx) + aucd, err := user.AlistCache().LoadOrStore(ctx, req.ServerID) if err != nil { if errors.Is(err, db.ErrNotFound("vendor")) { - ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist not login")) + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found")) return } @@ -65,12 +126,6 @@ func List(ctx *gin.Context) { return } - page, size, err := utils.GetPageAndMax(ctx) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - var cli = vendor.LoadAlistClient(ctx.Query("backend")) data, err := cli.FsList(ctx, &alist.FsListReq{ Token: aucd.Token, @@ -86,16 +141,24 @@ func List(ctx *gin.Context) { return } - req.Path = strings.TrimRight(req.Path, "/") + req.Path = strings.Trim(req.Path, "/") resp := AlistFSListResp{ Total: data.Total, - Paths: model.GenDefaultPaths(req.Path), + Paths: model.GenDefaultPaths(req.Path, true, + &model.Path{ + Name: "", + Path: "", + }, + &model.Path{ + Name: aucd.Host, + Path: aucd.ServerID + "/", + }), } for _, flr := range data.Content { resp.Items = append(resp.Items, &AlistFileItem{ Item: &model.Item{ Name: flr.Name, - Path: fmt.Sprintf("%s/%s", req.Path, flr.Name), + Path: fmt.Sprintf("%s/%s", aucd.ServerID, strings.Trim(fmt.Sprintf("%s/%s", req.Path, flr.Name), "/")), IsDir: flr.IsDir, }, Size: flr.Size, diff --git a/server/handlers/vendors/vendorAlist/login.go b/server/handlers/vendors/vendorAlist/login.go index f858c32..5ead696 100644 --- a/server/handlers/vendors/vendorAlist/login.go +++ b/server/handlers/vendors/vendorAlist/login.go @@ -6,6 +6,8 @@ import ( "encoding/hex" "errors" "net/http" + "net/url" + "strings" "github.com/gin-gonic/gin" json "github.com/json-iterator/go" @@ -27,6 +29,14 @@ func (r *LoginReq) Validate() error { if r.Host == "" { return errors.New("host is required") } + url, err := url.Parse(r.Host) + if err != nil { + return err + } + if url.Scheme != "http" && url.Scheme != "https" { + return errors.New("host is invalid") + } + r.Host = strings.TrimRight(url.String(), "/") if r.Password != "" && r.HashedPassword != "" { return errors.New("password and hashedPassword can't be both set") } @@ -67,8 +77,9 @@ func Login(ctx *gin.Context) { _, err = db.CreateOrSaveAlistVendor(&dbModel.AlistVendor{ UserID: user.ID, - Backend: backend, - Host: req.Host, + ServerID: data.ServerID, + Backend: data.Backend, + Host: data.Host, Username: req.Username, HashedPassword: []byte(req.HashedPassword), }) @@ -77,7 +88,7 @@ func Login(ctx *gin.Context) { return } - _, err = user.AlistCache().Data().Refresh(ctx, func(ctx context.Context, args ...struct{}) (*cache.AlistUserCacheData, error) { + _, err = user.AlistCache().StoreOrRefreshWithDynamicFunc(ctx, data.ServerID, func(ctx context.Context, key string, args ...struct{}) (*cache.AlistUserCacheData, error) { return data, nil }) if err != nil { @@ -91,13 +102,21 @@ func Login(ctx *gin.Context) { func Logout(ctx *gin.Context) { user := ctx.MustGet("user").(*op.User) - err := db.DeleteAlistVendor(user.ID) + var req model.ServerIDReq + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := db.DeleteAlistVendor(user.ID, req.ServerID) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return } - user.AlistCache().Clear() + if rc, ok := user.AlistCache().LoadCache(req.ServerID); ok { + rc.Clear() + } ctx.Status(http.StatusNoContent) } diff --git a/server/handlers/vendors/vendorAlist/me.go b/server/handlers/vendors/vendorAlist/me.go index afd8895..cb339be 100644 --- a/server/handlers/vendors/vendorAlist/me.go +++ b/server/handlers/vendors/vendorAlist/me.go @@ -17,12 +17,17 @@ type AlistMeResp = model.VendorMeResp[*alist.MeResp] func Me(ctx *gin.Context) { user := ctx.MustGet("user").(*op.User) - aucd, err := user.AlistCache().Get(ctx) + serverID := ctx.Query("serverID") + if serverID == "" { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(errors.New("serverID is required"))) + return + + } + + aucd, err := user.AlistCache().LoadOrStore(ctx, serverID) if err != nil { if errors.Is(err, db.ErrNotFound("vendor")) { - ctx.JSON(http.StatusOK, model.NewApiDataResp(&AlistMeResp{ - IsLogin: false, - })) + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found")) return } ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) @@ -43,3 +48,37 @@ func Me(ctx *gin.Context) { Info: resp, })) } + +type AlistBindsResp []*struct { + ServerID string `json:"serverID"` + Host string `json:"host"` +} + +func Binds(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + ev, err := db.GetAlistVendors(user.ID) + if err != nil { + if errors.Is(err, db.ErrNotFound("vendor")) { + ctx.JSON(http.StatusOK, model.NewApiDataResp(&AlistMeResp{ + IsLogin: false, + })) + return + } + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + var resp AlistBindsResp = make(AlistBindsResp, len(ev)) + for i, v := range ev { + resp[i] = &struct { + ServerID string "json:\"serverID\"" + Host string "json:\"host\"" + }{ + ServerID: v.ServerID, + Host: v.Host, + } + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(resp)) +} diff --git a/server/handlers/vendors/vendorEmby/list.go b/server/handlers/vendors/vendorEmby/list.go index 52078a4..fd73d21 100644 --- a/server/handlers/vendors/vendorEmby/list.go +++ b/server/handlers/vendors/vendorEmby/list.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" "strconv" - "strings" "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" @@ -24,10 +24,13 @@ type ListReq struct { Keywords string `json:"keywords"` } -func (r *ListReq) Validate() error { - if s := strings.Split(r.Path, "/"); len(s) == 2 { - r.ServerID = s[0] - r.Path = s[1] +func (r *ListReq) Validate() (err error) { + if r.Path == "" { + return nil + } + r.ServerID, r.Path, err = dbModel.GetEmbyServerIdFromPath(r.Path) + if err != nil { + return err } if r.Path == "" { return nil @@ -79,7 +82,7 @@ func List(ctx *gin.Context) { ev, err := db.GetEmbyVendors(user.ID, append(socpes, db.Paginate(page, size))...) if err != nil { if errors.Is(err, db.ErrNotFound("vendor")) { - ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login")) + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found")) return } ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) @@ -121,7 +124,7 @@ func List(ctx *gin.Context) { aucd, err := user.EmbyCache().LoadOrStore(ctx, req.ServerID) if err != nil { if errors.Is(err, db.ErrNotFound("vendor")) { - ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login")) + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found")) return } ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) diff --git a/server/handlers/vendors/vendorEmby/login.go b/server/handlers/vendors/vendorEmby/login.go index 22cf050..3923046 100644 --- a/server/handlers/vendors/vendorEmby/login.go +++ b/server/handlers/vendors/vendorEmby/login.go @@ -4,6 +4,8 @@ import ( "context" "errors" "net/http" + "net/url" + "strings" "github.com/gin-gonic/gin" json "github.com/json-iterator/go" @@ -27,6 +29,14 @@ func (r *LoginReq) Validate() error { if r.Host == "" { return errors.New("host is required") } + url, err := url.Parse(r.Host) + if err != nil { + return err + } + if url.Scheme != "http" && url.Scheme != "https" { + return errors.New("host is invalid") + } + r.Host = strings.TrimRight(url.String(), "/") if r.ApiKey == "" && (r.Username == "" || r.Password == "") { return errors.New("username and password or apiKey is required") } @@ -108,25 +118,10 @@ func Login(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } -type LogoutReq struct { - ServerID string `json:"serverId"` -} - -func (r *LogoutReq) Validate() error { - if r.ServerID == "" { - return errors.New("serverId is required") - } - return nil -} - -func (r *LogoutReq) Decode(ctx *gin.Context) error { - return json.NewDecoder(ctx.Request.Body).Decode(r) -} - func Logout(ctx *gin.Context) { user := ctx.MustGet("user").(*op.User) - var req LogoutReq + var req model.ServerIDReq if err := model.Decode(ctx, &req); err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return diff --git a/server/handlers/vendors/vendorEmby/me.go b/server/handlers/vendors/vendorEmby/me.go index 316034c..f4052f3 100644 --- a/server/handlers/vendors/vendorEmby/me.go +++ b/server/handlers/vendors/vendorEmby/me.go @@ -26,6 +26,10 @@ func Me(ctx *gin.Context) { eucd, err := user.EmbyCache().LoadOrStore(ctx, serverID) if err != nil { + if errors.Is(err, db.ErrNotFound("vendor")) { + ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found")) + return + } ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return } diff --git a/server/model/vendor.go b/server/model/vendor.go index 4d6a109..142703c 100644 --- a/server/model/vendor.go +++ b/server/model/vendor.go @@ -1,8 +1,12 @@ package model import ( + "errors" "fmt" "strings" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" ) type VendorMeResp[T any] struct { @@ -16,14 +20,16 @@ type VendorFSListResp[T any] struct { Total uint64 `json:"total"` } -func GenDefaultPaths(path string) []*Path { - paths := []*Path{} +func GenDefaultPaths(path string, skipEmpty bool, paths ...*Path) []*Path { path = strings.TrimRight(path, "/") - for i, v := range strings.Split(path, `/`) { - if i != 0 { + for _, v := range strings.Split(path, `/`) { + if skipEmpty && v == "" { + continue + } + if l := len(paths); l != 0 { paths = append(paths, &Path{ Name: v, - Path: fmt.Sprintf("%s/%s", paths[i-1].Path, v), + Path: fmt.Sprintf("%s/%s", strings.TrimRight(paths[l-1].Path, "/"), v), }) } else { paths = append(paths, &Path{ @@ -45,3 +51,18 @@ type Item struct { Path string `json:"path"` IsDir bool `json:"isDir"` } + +type ServerIDReq struct { + ServerID string `json:"serverId"` +} + +func (r *ServerIDReq) Validate() error { + if r.ServerID == "" { + return errors.New("serverId is required") + } + return nil +} + +func (r *ServerIDReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} diff --git a/utils/utils.go b/utils/utils.go index 98a60ea..0e17c3a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -310,7 +310,10 @@ func LIKE(s string) string { } func SortUUID() string { - src := uuid.New() + return SortUUIDWithUUID(uuid.New()) +} + +func SortUUIDWithUUID(src uuid.UUID) string { dst := make([]byte, 32) hex.Encode(dst, src[:]) return stream.BytesToString(dst)