Feat: support mutil emby client

pull/52/head
zijiren233 2 years ago
parent 08d348961e
commit 7d9f4417ae

@ -40,7 +40,7 @@ func init() {
home = "~" home = "~"
} }
RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data-dir", filepath.Join(home, ".synctv"), "data dir") RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data-dir", filepath.Join(home, ".synctv"), "data dir")
RootCmd.PersistentFlags().BoolVar(&flags.ForceAutoMigrate, "force-auto-migrate", false, "force auto migrate") RootCmd.PersistentFlags().BoolVar(&flags.ForceAutoMigrate, "force-auto-migrate", version.Version == "dev", "force auto migrate")
} }
func init() { func init() {

@ -30,7 +30,7 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/soheilhy/cmux v0.1.5 github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/synctv-org/vendors v0.2.2-0.20231226121731-35912737bbb2 github.com/synctv-org/vendors v0.2.2-0.20231227065102-90bfed7f8a05
github.com/ulule/limiter/v3 v3.11.2 github.com/ulule/limiter/v3 v3.11.2
github.com/zencoder/go-dash/v3 v3.0.3 github.com/zencoder/go-dash/v3 v3.0.3
github.com/zijiren233/gencontainer v0.0.0-20231213075414-f7f4c8261dca github.com/zijiren233/gencontainer v0.0.0-20231213075414-f7f4c8261dca

@ -350,8 +350,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/synctv-org/vendors v0.2.2-0.20231226121731-35912737bbb2 h1:c9R7idpWvnqKvMxGkzQvJMoQ8os1shy+rgaawjk6xKw= github.com/synctv-org/vendors v0.2.2-0.20231227065102-90bfed7f8a05 h1:cWpk2uk9P6K1kuwnLy5Zay0GifYk3ss5bxUOMLb0yV4=
github.com/synctv-org/vendors v0.2.2-0.20231226121731-35912737bbb2/go.mod h1:Q+KEUh8ZgCSMjY5rCfz44+7POlHcGttZ/bYFiTDVBds= github.com/synctv-org/vendors v0.2.2-0.20231227065102-90bfed7f8a05/go.mod h1:Q+KEUh8ZgCSMjY5rCfz44+7POlHcGttZ/bYFiTDVBds=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=

@ -130,8 +130,8 @@ func BilibiliSharedMpdCacheInitFunc(ctx context.Context, movie *model.Movie, arg
}, nil }, nil
} }
func NewBilibiliNoSharedMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, args ...*BilibiliUserCache) (string, error) { func NewBilibiliNoSharedMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, key string, args ...*BilibiliUserCache) (string, error) {
return func(ctx context.Context, args ...*BilibiliUserCache) (string, error) { return func(ctx context.Context, key string, args ...*BilibiliUserCache) (string, error) {
return BilibiliNoSharedMovieCacheInitFunc(ctx, movie, args...) return BilibiliNoSharedMovieCacheInitFunc(ctx, movie, args...)
} }
} }

@ -9,7 +9,7 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
type MapRefreshFunc[T any, A any] func(ctx context.Context, args ...A) (T, error) type MapRefreshFunc[T any, A any] func(ctx context.Context, key string, args ...A) (T, error)
type MapCache[T any, A any] struct { type MapCache[T any, A any] struct {
lock sync.RWMutex lock sync.RWMutex
@ -36,82 +36,137 @@ func (b *MapCache[T, A]) clear() {
maps.Clear(b.cache) maps.Clear(b.cache)
} }
func (b *MapCache[T, A]) LoadOrStore(ctx context.Context, id string, args ...A) (T, error) { func (b *MapCache[T, A]) Delete(key string) {
b.lock.Lock()
defer b.lock.Unlock()
delete(b.cache, key)
}
func (b *MapCache[T, A]) LoadOrStore(ctx context.Context, key string, args ...A) (T, error) {
b.lock.RLock() b.lock.RLock()
c, loaded := b.cache[id] c, loaded := b.cache[key]
if loaded { if loaded {
b.lock.RUnlock() b.lock.RUnlock()
return c.Get(ctx, args...) return c.Get(ctx, args...)
} }
b.lock.RUnlock() b.lock.RUnlock()
b.lock.Lock() b.lock.Lock()
c, loaded = b.cache[id] c, loaded = b.cache[key]
if loaded { if loaded {
b.lock.Unlock() b.lock.Unlock()
return c.Get(ctx, args...) return c.Get(ctx, args...)
} }
c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](b.refreshFunc), b.maxAge) c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
b.cache[id] = c return b.refreshFunc(ctx, key, args...)
}), b.maxAge)
b.cache[key] = c
b.lock.Unlock() b.lock.Unlock()
return c.Get(ctx, args...) return c.Get(ctx, args...)
} }
func (b *MapCache[T, A]) StoreOrRefresh(ctx context.Context, id string, args ...A) (T, error) { func (b *MapCache[T, A]) StoreOrRefresh(ctx context.Context, key string, args ...A) (T, error) {
b.lock.RLock() b.lock.RLock()
c, ok := b.cache[id] c, ok := b.cache[key]
if ok { if ok {
b.lock.RUnlock() b.lock.RUnlock()
return c.Refresh(ctx, args...) return c.Refresh(ctx, args...)
} }
b.lock.RUnlock() b.lock.RUnlock()
b.lock.Lock() b.lock.Lock()
c, ok = b.cache[id] c, ok = b.cache[key]
if ok { if ok {
b.lock.Unlock() b.lock.Unlock()
return c.Refresh(ctx, args...) return c.Refresh(ctx, args...)
} }
c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](b.refreshFunc), b.maxAge) c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
b.cache[id] = c return b.refreshFunc(ctx, key, args...)
}), b.maxAge)
b.cache[key] = c
b.lock.Unlock() b.lock.Unlock()
return c.Refresh(ctx, args...) return c.Refresh(ctx, args...)
} }
func (b *MapCache[T, A]) LoadOrStoreWithDynamicFunc(ctx context.Context, id string, refreshFunc MapRefreshFunc[T, A], args ...A) (T, error) { func (b *MapCache[T, A]) LoadCache(key string) (*refreshcache.RefreshCache[T, A], bool) {
b.lock.RLock()
c, ok := b.cache[key]
b.lock.RUnlock()
return c, ok
}
func (b *MapCache[T, A]) LoadOrNewCache(key string) *refreshcache.RefreshCache[T, A] {
b.lock.RLock()
c, ok := b.cache[key]
if ok {
b.lock.RUnlock()
return c
}
b.lock.RUnlock()
b.lock.Lock()
c, ok = b.cache[key]
if ok {
b.lock.Unlock()
return c
}
c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return b.refreshFunc(ctx, key, args...)
}), b.maxAge)
b.cache[key] = c
b.lock.Unlock()
return c
}
func (b *MapCache[T, A]) LoadOrStoreWithDynamicFunc(ctx context.Context, key string, refreshFunc MapRefreshFunc[T, A], args ...A) (T, error) {
b.lock.RLock() b.lock.RLock()
c, loaded := b.cache[id] c, loaded := b.cache[key]
if loaded { if loaded {
b.lock.RUnlock() b.lock.RUnlock()
return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }
b.lock.RUnlock() b.lock.RUnlock()
b.lock.Lock() b.lock.Lock()
c, loaded = b.cache[id] c, loaded = b.cache[key]
if loaded { if loaded {
b.lock.Unlock() b.lock.Unlock()
return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }
c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](b.refreshFunc), b.maxAge) c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
b.cache[id] = c return b.refreshFunc(ctx, key, args...)
}), b.maxAge)
b.cache[key] = c
b.lock.Unlock() b.lock.Unlock()
return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Get(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }
func (b *MapCache[T, A]) StoreOrRefreshWithDynamicFunc(ctx context.Context, id string, refreshFunc MapRefreshFunc[T, A], args ...A) (T, error) { func (b *MapCache[T, A]) StoreOrRefreshWithDynamicFunc(ctx context.Context, key string, refreshFunc MapRefreshFunc[T, A], args ...A) (T, error) {
b.lock.RLock() b.lock.RLock()
c, ok := b.cache[id] c, ok := b.cache[key]
if ok { if ok {
b.lock.RUnlock() b.lock.RUnlock()
return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }
b.lock.RUnlock() b.lock.RUnlock()
b.lock.Lock() b.lock.Lock()
c, ok = b.cache[id] c, ok = b.cache[key]
if ok { if ok {
b.lock.Unlock() b.lock.Unlock()
return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }
c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](b.refreshFunc), b.maxAge) c = refreshcache.NewRefreshCache[T, A](refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
b.cache[id] = c return b.refreshFunc(ctx, key, args...)
}), b.maxAge)
b.cache[key] = c
b.lock.Unlock() b.lock.Unlock()
return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](refreshFunc), args...) return c.Data().Refresh(ctx, refreshcache.RefreshFunc[T, A](func(ctx context.Context, args ...A) (T, error) {
return refreshFunc(ctx, key, args...)
}), args...)
} }

@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/db"
"github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/model"
@ -16,36 +17,43 @@ import (
"github.com/zijiren233/gencontainer/refreshcache" "github.com/zijiren233/gencontainer/refreshcache"
) )
type EmbyUserCache = refreshcache.RefreshCache[*EmbyUserCacheData, struct{}] type EmbyUserCache = MapCache[*EmbyUserCacheData, struct{}]
type EmbyUserCacheData struct { type EmbyUserCacheData struct {
Host string Host string
ApiKey string ServerID string
Backend string ApiKey string
Backend string
} }
func NewEmbyUserCache(userID string) *EmbyUserCache { func NewEmbyUserCache(userID string) *EmbyUserCache {
f := EmbyAuthorizationCacheWithUserIDInitFunc(userID) return newMapCache(func(ctx context.Context, key string, args ...struct{}) (*EmbyUserCacheData, error) {
return refreshcache.NewRefreshCache(func(ctx context.Context, args ...struct{}) (*EmbyUserCacheData, error) { return EmbyAuthorizationCacheWithUserIDInitFunc(userID, key)
return f(ctx)
}, 0) }, 0)
} }
func EmbyAuthorizationCacheWithUserIDInitFunc(userID string) func(ctx context.Context, args ...struct{}) (*EmbyUserCacheData, error) { func EmbyAuthorizationCacheWithUserIDInitFunc(userID, serverID string) (*EmbyUserCacheData, error) {
return func(ctx context.Context, args ...struct{}) (*EmbyUserCacheData, error) { var (
v, err := db.GetEmbyVendor(userID) v *model.EmbyVendor
if err != nil { err error
return nil, err )
} if serverID == "" {
if v.ApiKey == "" || v.Host == "" { v, err = db.GetEmbyFirstVendor(userID)
return nil, db.ErrNotFound("vendor") } else {
} v, err = db.GetEmbyVendor(userID, serverID)
return &EmbyUserCacheData{ }
Host: v.Host, if err != nil {
ApiKey: v.ApiKey, return nil, err
Backend: v.Backend,
}, nil
} }
if v.ApiKey == "" || v.Host == "" {
return nil, db.ErrNotFound("vendor")
}
return &EmbyUserCacheData{
Host: v.Host,
ServerID: v.ServerID,
ApiKey: v.ApiKey,
Backend: v.Backend,
}, nil
} }
type EmbySource struct { type EmbySource struct {
@ -76,7 +84,16 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
if len(args) == 0 { if len(args) == 0 {
return nil, errors.New("need emby user cache") return nil, errors.New("need emby user cache")
} }
aucd, err := args[0].Get(ctx)
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")
}
aucd, err := args[0].LoadOrStore(ctx, serverID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -91,7 +108,7 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
data, err := cli.GetItem(ctx, &emby.GetItemReq{ data, err := cli.GetItem(ctx, &emby.GetItemReq{
Host: aucd.Host, Host: aucd.Host,
Token: aucd.ApiKey, Token: aucd.ApiKey,
ItemId: movie.Base.VendorInfo.Emby.Path, ItemId: itemID,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

@ -48,7 +48,7 @@ func UpdateMovie(movie *model.Movie, columns ...clause.Column) error {
} }
func SaveMovie(movie *model.Movie, columns ...clause.Column) error { func SaveMovie(movie *model.Movie, columns ...clause.Column) error {
err := db.Model(movie).Clauses(clause.Returning{Columns: columns}).Where("room_id = ? AND id = ?", movie.RoomID, movie.ID).Save(movie).Error err := db.Model(movie).Clauses(clause.Returning{Columns: columns}).Where("room_id = ? AND id = ?", movie.RoomID, movie.ID).Omit("created_at").Save(movie).Error
return HandleNotFound(err, "room or movie") return HandleNotFound(err, "room or movie")
} }
@ -65,10 +65,10 @@ func SwapMoviePositions(roomID, movie1ID, movie2ID string) (err error) {
return HandleNotFound(err, "movie2") return HandleNotFound(err, "movie2")
} }
movie1.Position, movie2.Position = movie2.Position, movie1.Position movie1.Position, movie2.Position = movie2.Position, movie1.Position
err = tx.Save(movie1).Error err = tx.Omit("created_at").Save(movie1).Error
if err != nil { if err != nil {
return err return err
} }
return tx.Save(movie2).Error return tx.Omit("created_at").Save(movie2).Error
}) })
} }

@ -33,42 +33,57 @@ var models = []any{
var dbVersions = map[string]dbVersion{ var dbVersions = map[string]dbVersion{
"0.0.1": { "0.0.1": {
NextVersion: "0.0.2", NextVersion: "0.0.2",
Upgrade: nil,
},
"0.0.2": {
NextVersion: "0.0.3",
Upgrade: func(db *gorm.DB) error { Upgrade: func(db *gorm.DB) error {
return db.Migrator().DropTable("streaming_vendor_infos") return db.Migrator().DropTable("streaming_vendor_infos")
}, },
}, },
"0.0.2": { "0.0.3": {
NextVersion: "", NextVersion: "",
Upgrade: nil, Upgrade: func(db *gorm.DB) error {
return db.Migrator().DropTable("alist_vendors", "emby_vendors")
},
}, },
} }
func UpgradeDatabase() error { func UpgradeDatabase() error {
var currentVersion string if conf.Conf.Database.Type == conf.DatabaseTypeMysql {
if db.Migrator().HasTable(&model.Setting{}) { if err := db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
setting := model.Setting{
Name: "database_version",
Type: model.SettingTypeString,
Group: model.SettingGroupDatabase,
Value: CurrentVersion,
}
err := FirstOrCreateSettingItemValue(&setting)
if err != nil {
return err return err
} }
currentVersion = setting.Value defer func() {
if flags.ForceAutoMigrate || currentVersion != CurrentVersion { err := db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error
if err != nil {
log.Fatalf("failed to set foreign key checks: %s", err.Error())
}
}()
}
if !db.Migrator().HasTable(&model.Setting{}) {
return autoMigrate(models...)
}
setting := model.Setting{
Name: "database_version",
Type: model.SettingTypeString,
Group: model.SettingGroupDatabase,
Value: CurrentVersion,
}
err := FirstOrCreateSettingItemValue(&setting)
if err != nil {
return err
}
currentVersion := setting.Value
if flags.ForceAutoMigrate || currentVersion != CurrentVersion {
defer func() {
err = autoMigrate(models...) err = autoMigrate(models...)
if err != nil { if err != nil {
return err log.Fatalf("failed to auto migrate: %s", err.Error())
} }
} }()
} else {
err := autoMigrate(models...)
if err != nil {
return err
}
currentVersion = CurrentVersion
} }
version, ok := dbVersions[currentVersion] version, ok := dbVersions[currentVersion]
@ -101,13 +116,7 @@ func autoMigrate(dst ...any) error {
log.Info("migrating database...") log.Info("migrating database...")
switch conf.Conf.Database.Type { switch conf.Conf.Database.Type {
case conf.DatabaseTypeMysql: case conf.DatabaseTypeMysql:
if err := db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil { return db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").AutoMigrate(dst...)
return err
}
if err := db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").AutoMigrate(dst...); err != nil {
return err
}
return db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error
case conf.DatabaseTypeSqlite3, conf.DatabaseTypePostgres: case conf.DatabaseTypeSqlite3, conf.DatabaseTypePostgres:
return db.AutoMigrate(dst...) return db.AutoMigrate(dst...)
default: default:

@ -270,7 +270,7 @@ func LoadAndDeleteUserByID(userID string, columns ...clause.Column) (*model.User
} }
func SaveUser(u *model.User) error { func SaveUser(u *model.User) error {
return db.Save(u).Error return db.Omit("created_at").Save(u).Error
} }
func AddAdmin(u *model.User) error { func AddAdmin(u *model.User) error {

@ -52,7 +52,7 @@ func CreateOrSaveVendorBackend(backend *model.VendorBackend) (*model.VendorBacke
if err := tx.Where("backend_endpoint = ?", backend.Backend.Endpoint).First(&model.VendorBackend{}).Error; errors.Is(err, gorm.ErrRecordNotFound) { if err := tx.Where("backend_endpoint = ?", backend.Backend.Endpoint).First(&model.VendorBackend{}).Error; errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Create(&backend).Error return tx.Create(&backend).Error
} else { } else {
return tx.Save(&backend).Error return tx.Omit("created_at").Save(&backend).Error
} }
}) })
} }

@ -13,15 +13,17 @@ func GetBilibiliVendor(userID string) (*model.BilibiliVendor, error) {
return &vendor, HandleNotFound(err, "vendor") return &vendor, HandleNotFound(err, "vendor")
} }
func CreateOrSaveBilibiliVendor(userID string, vendorInfo *model.BilibiliVendor) (*model.BilibiliVendor, error) { func CreateOrSaveBilibiliVendor(vendorInfo *model.BilibiliVendor) (*model.BilibiliVendor, error) {
vendorInfo.UserID = userID if vendorInfo.UserID == "" {
return nil, errors.New("user_id must not be empty")
}
return vendorInfo, Transactional(func(tx *gorm.DB) error { return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.BilibiliVendor{ if errors.Is(tx.First(&model.BilibiliVendor{
UserID: userID, UserID: vendorInfo.UserID,
}).Error, gorm.ErrRecordNotFound) { }).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error return tx.Create(&vendorInfo).Error
} else { } else {
return tx.Save(&vendorInfo).Error return tx.Omit("created_at").Save(&vendorInfo).Error
} }
}) })
} }
@ -36,15 +38,17 @@ func GetAlistVendor(userID string) (*model.AlistVendor, error) {
return &vendor, HandleNotFound(err, "vendor") return &vendor, HandleNotFound(err, "vendor")
} }
func CreateOrSaveAlistVendor(userID string, vendorInfo *model.AlistVendor) (*model.AlistVendor, error) { func CreateOrSaveAlistVendor(vendorInfo *model.AlistVendor) (*model.AlistVendor, error) {
vendorInfo.UserID = userID if vendorInfo.UserID == "" {
return nil, errors.New("user_id must not be empty")
}
return vendorInfo, Transactional(func(tx *gorm.DB) error { return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.AlistVendor{ if errors.Is(tx.First(&model.AlistVendor{
UserID: userID, UserID: vendorInfo.UserID,
}).Error, gorm.ErrRecordNotFound) { }).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error return tx.Create(&vendorInfo).Error
} else { } else {
return tx.Save(&vendorInfo).Error return tx.Omit("created_at").Save(&vendorInfo).Error
} }
}) })
} }
@ -53,25 +57,46 @@ func DeleteAlistVendor(userID string) error {
return db.Where("user_id = ?", userID).Delete(&model.AlistVendor{}).Error return db.Where("user_id = ?", userID).Delete(&model.AlistVendor{}).Error
} }
func GetEmbyVendor(userID string) (*model.EmbyVendor, error) { func GetEmbyVendors(userID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.EmbyVendor, error) {
var vendors []*model.EmbyVendor
err := db.Scopes(scopes...).Where("user_id = ?", userID).Find(&vendors).Error
return vendors, err
}
func GetEmbyVendorsCount(userID string, scopes ...func(*gorm.DB) *gorm.DB) (int64, error) {
var count int64
err := db.Scopes(scopes...).Where("user_id = ?", userID).Model(&model.EmbyVendor{}).Count(&count).Error
return count, err
}
func GetEmbyVendor(userID, serverID string) (*model.EmbyVendor, error) {
var vendor model.EmbyVendor
err := db.Where("user_id = ? AND server_id = ?", userID, serverID).First(&vendor).Error
return &vendor, HandleNotFound(err, "vendor")
}
func GetEmbyFirstVendor(userID string) (*model.EmbyVendor, error) {
var vendor model.EmbyVendor var vendor model.EmbyVendor
err := db.Where("user_id = ?", userID).First(&vendor).Error err := db.Where("user_id = ?", userID).First(&vendor).Error
return &vendor, HandleNotFound(err, "vendor") return &vendor, HandleNotFound(err, "vendor")
} }
func CreateOrSaveEmbyVendor(userID string, vendorInfo *model.EmbyVendor) (*model.EmbyVendor, error) { func CreateOrSaveEmbyVendor(vendorInfo *model.EmbyVendor) (*model.EmbyVendor, error) {
vendorInfo.UserID = userID 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 { return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.EmbyVendor{ if errors.Is(tx.First(&model.EmbyVendor{
UserID: userID, UserID: vendorInfo.UserID,
ServerID: vendorInfo.ServerID,
}).Error, gorm.ErrRecordNotFound) { }).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error return tx.Create(&vendorInfo).Error
} else { } else {
return tx.Save(&vendorInfo).Error return tx.Omit("created_at").Save(&vendorInfo).Error
} }
}) })
} }
func DeleteEmbyVendor(userID string) error { func DeleteEmbyVendor(userID, serverID string) error {
return db.Where("user_id = ?", userID).Delete(&model.EmbyVendor{}).Error return db.Where("user_id = ? AND server_id = ?", userID, serverID).Delete(&model.EmbyVendor{}).Error
} }

@ -89,6 +89,13 @@ type AlistStreamingInfo struct {
Password string `gorm:"type:varchar(256)" json:"password,omitempty"` Password string `gorm:"type:varchar(256)" json:"password,omitempty"`
} }
func (a *AlistStreamingInfo) Validate() error {
if a.Path == "" {
return fmt.Errorf("path is empty")
}
return nil
}
func (a *AlistStreamingInfo) BeforeSave(tx *gorm.DB) error { func (a *AlistStreamingInfo) BeforeSave(tx *gorm.DB) error {
if a.Password != "" { if a.Password != "" {
s, err := utils.CryptoToBase64([]byte(a.Password), utils.GenCryptoKey(a.Path)) s, err := utils.CryptoToBase64([]byte(a.Password), utils.GenCryptoKey(a.Path))
@ -116,5 +123,12 @@ func (a *AlistStreamingInfo) AfterFind(tx *gorm.DB) error {
} }
type EmbyStreamingInfo struct { type EmbyStreamingInfo struct {
Path string `gorm:"type:varchar(20)" json:"path,omitempty"` Path string `gorm:"type:varchar(52)" json:"path,omitempty"`
}
func (e *EmbyStreamingInfo) Validate() error {
if e.Path == "" {
return fmt.Errorf("path is empty")
}
return nil
} }

@ -1,5 +1,7 @@
package model package model
import "time"
type SettingType string type SettingType string
const ( const (
@ -26,8 +28,9 @@ const (
) )
type Setting struct { type Setting struct {
Name string `gorm:"primaryKey;type:varchar(256)"` Name string `gorm:"primaryKey;type:varchar(256)"`
Value string `gorm:"not null;type:text"` UpdatedAt time.Time
Type SettingType `gorm:"not null;default:string"` Value string `gorm:"not null;type:text"`
Group SettingGroup `gorm:"not null"` Type SettingType `gorm:"not null;default:string"`
Group SettingGroup `gorm:"not null"`
} }

@ -52,7 +52,7 @@ type User struct {
Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` 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"` EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
} }
func (u *User) CheckPassword(password string) bool { func (u *User) CheckPassword(password string) bool {

@ -50,9 +50,11 @@ func (b *Backend) Validate() error {
} }
type VendorBackend struct { type VendorBackend struct {
Backend Backend `gorm:"embedded;embeddedPrefix:backend_" json:"backend"` CreatedAt time.Time
Enabled bool `gorm:"default:true" json:"enabled"` UpdatedAt time.Time
UsedBy BackendUsedBy `gorm:"embedded;embeddedPrefix:used_by_" json:"usedBy"` Backend Backend `gorm:"embedded;embeddedPrefix:backend_" json:"backend"`
Enabled bool `gorm:"default:true" json:"enabled"`
UsedBy BackendUsedBy `gorm:"embedded;embeddedPrefix:used_by_" json:"usedBy"`
} }
type BackendUsedBy struct { type BackendUsedBy struct {

@ -1,14 +1,18 @@
package model package model
import ( import (
"time"
"github.com/synctv-org/synctv/utils" "github.com/synctv-org/synctv/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
type BilibiliVendor struct { type BilibiliVendor struct {
UserID string `gorm:"primaryKey;type:char(32)"` CreatedAt time.Time
Backend string `gorm:"type:varchar(64)"` UpdatedAt time.Time
Cookies map[string]string `gorm:"serializer:fastjson;type:text"` UserID string `gorm:"primaryKey;type:char(32)"`
Backend string `gorm:"type:varchar(64)"`
Cookies map[string]string `gorm:"not null;serializer:fastjson;type:text"`
} }
func (b *BilibiliVendor) BeforeSave(tx *gorm.DB) error { func (b *BilibiliVendor) BeforeSave(tx *gorm.DB) error {
@ -40,9 +44,11 @@ func (b *BilibiliVendor) AfterFind(tx *gorm.DB) error {
} }
type AlistVendor struct { type AlistVendor struct {
CreatedAt time.Time
UpdatedAt time.Time
UserID string `gorm:"primaryKey;type:char(32)"` UserID string `gorm:"primaryKey;type:char(32)"`
Backend string `gorm:"type:varchar(64)"` Backend string `gorm:"type:varchar(64)"`
Host string `gorm:"type:varchar(256)"` Host string `gorm:"not null;type:varchar(256)"`
Username string `gorm:"type:varchar(256)"` Username string `gorm:"type:varchar(256)"`
HashedPassword []byte HashedPassword []byte
} }
@ -87,14 +93,17 @@ func (a *AlistVendor) AfterFind(tx *gorm.DB) error {
} }
type EmbyVendor struct { type EmbyVendor struct {
UserID string `gorm:"primaryKey;type:char(32)"` CreatedAt time.Time
Backend string `gorm:"type:varchar(64)"` UpdatedAt time.Time
Host string `gorm:"type:varchar(256)"` UserID string `gorm:"primaryKey;type:char(32)"`
ApiKey string `gorm:"type:varchar(256)"` Backend string `gorm:"type:varchar(64)"`
ServerID string `gorm:"primaryKey;type:char(32)"`
Host string `gorm:"not null;type:varchar(256)"`
ApiKey string `gorm:"not null;type:varchar(256)"`
} }
func (e *EmbyVendor) BeforeSave(tx *gorm.DB) error { func (e *EmbyVendor) BeforeSave(tx *gorm.DB) error {
key := utils.GenCryptoKey(e.UserID) key := utils.GenCryptoKey(e.ServerID)
var err error var err error
if e.Host, err = utils.CryptoToBase64([]byte(e.Host), key); err != nil { if e.Host, err = utils.CryptoToBase64([]byte(e.Host), key); err != nil {
return err return err
@ -106,7 +115,7 @@ func (e *EmbyVendor) BeforeSave(tx *gorm.DB) error {
} }
func (e *EmbyVendor) AfterSave(tx *gorm.DB) error { func (e *EmbyVendor) AfterSave(tx *gorm.DB) error {
key := utils.GenCryptoKey(e.UserID) key := utils.GenCryptoKey(e.ServerID)
if v, err := utils.DecryptoFromBase64(e.Host, key); err != nil { if v, err := utils.DecryptoFromBase64(e.Host, key); err != nil {
return err return err
} else { } else {

@ -225,9 +225,10 @@ func (movie *Movie) validateVendorMovie() error {
return movie.Movie.Base.VendorInfo.Bilibili.Validate() return movie.Movie.Base.VendorInfo.Bilibili.Validate()
case model.VendorAlist: case model.VendorAlist:
// return movie.Movie.Base.VendorInfo.Alist.Validate() return movie.Movie.Base.VendorInfo.Alist.Validate()
case model.VendorEmby: case model.VendorEmby:
return movie.Movie.Base.VendorInfo.Emby.Validate()
default: default:
return fmt.Errorf("vendor not implement validate") return fmt.Errorf("vendor not implement validate")

@ -257,5 +257,7 @@ func initVendor(vendor *gin.RouterGroup) {
emby.POST("/list", vendorEmby.List) emby.POST("/list", vendorEmby.List)
emby.GET("/me", vendorEmby.Me) emby.GET("/me", vendorEmby.Me)
emby.GET("/binds", vendorEmby.Binds)
} }
} }

@ -65,7 +65,8 @@ func Login(ctx *gin.Context) {
return return
} }
_, err = db.CreateOrSaveAlistVendor(user.ID, &dbModel.AlistVendor{ _, err = db.CreateOrSaveAlistVendor(&dbModel.AlistVendor{
UserID: user.ID,
Backend: backend, Backend: backend,
Host: req.Host, Host: req.Host,
Username: req.Username, Username: req.Username,

@ -76,7 +76,8 @@ func LoginWithQR(ctx *gin.Context) {
})) }))
return return
case bilibili.QRCodeStatus_SUCCESS: case bilibili.QRCodeStatus_SUCCESS:
_, err = db.CreateOrSaveBilibiliVendor(user.ID, &dbModel.BilibiliVendor{ _, err = db.CreateOrSaveBilibiliVendor(&dbModel.BilibiliVendor{
UserID: user.ID,
Cookies: resp.Cookies, Cookies: resp.Cookies,
Backend: backend, Backend: backend,
}) })
@ -203,7 +204,8 @@ func LoginWithSMS(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
} }
_, err = db.CreateOrSaveBilibiliVendor(user.ID, &dbModel.BilibiliVendor{ _, err = db.CreateOrSaveBilibiliVendor(&dbModel.BilibiliVendor{
UserID: user.ID,
Backend: backend, Backend: backend,
Cookies: c.Cookies, Cookies: c.Cookies,
}) })

@ -2,8 +2,10 @@ package vendorEmby
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
json "github.com/json-iterator/go" json "github.com/json-iterator/go"
@ -13,14 +15,20 @@ import (
"github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils" "github.com/synctv-org/synctv/utils"
"github.com/synctv-org/vendors/api/emby" "github.com/synctv-org/vendors/api/emby"
"gorm.io/gorm"
) )
type ListReq struct { type ListReq struct {
ServerID string `json:"-"`
Path string `json:"path"` Path string `json:"path"`
Keywords string `json:"keywords"` Keywords string `json:"keywords"`
} }
func (r *ListReq) Validate() error { func (r *ListReq) Validate() error {
if s := strings.Split(r.Path, "/"); len(s) == 2 {
r.ServerID = s[0]
r.Path = s[1]
}
if r.Path == "" { if r.Path == "" {
return nil return nil
} }
@ -54,19 +62,69 @@ func List(ctx *gin.Context) {
return return
} }
aucd, err := user.EmbyCache().Get(ctx) page, size, err := utils.GetPageAndMax(ctx)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login")) return
}
if req.ServerID == "" {
if req.Keywords != "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("keywords is not supported when not choose server (server id is empty)"))
return return
} }
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) socpes := [](func(*gorm.DB) *gorm.DB){
db.OrderByCreatedAtAsc,
}
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"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
total, err := db.GetEmbyVendorsCount(user.ID, socpes...)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
resp := EmbyFSListResp{
Paths: []*model.Path{
{
Name: "",
Path: "",
},
},
Total: uint64(total),
}
for _, evi := range ev {
resp.Items = append(resp.Items, &EmbyFileItem{
Item: &model.Item{
Name: evi.Host,
Path: evi.ServerID + `/`,
IsDir: true,
},
Type: "server",
})
}
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
return return
} }
page, size, err := utils.GetPageAndMax(ctx) aucd, err := user.EmbyCache().LoadOrStore(ctx, req.ServerID)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
} }
@ -84,22 +142,26 @@ func List(ctx *gin.Context) {
return return
} }
var resp EmbyFSListResp var resp EmbyFSListResp = EmbyFSListResp{
Paths: []*model.Path{
{},
},
}
for _, p := range data.Paths { for _, p := range data.Paths {
var n = p.Name var n = p.Name
if p.Path == "1" { if p.Path == "1" {
n = "" n = aucd.Host
} }
resp.Paths = append(resp.Paths, &model.Path{ resp.Paths = append(resp.Paths, &model.Path{
Name: n, Name: n,
Path: p.Path, Path: fmt.Sprintf("%s/%s", aucd.ServerID, p.Path),
}) })
} }
for _, i := range data.Items { for _, i := range data.Items {
resp.Items = append(resp.Items, &EmbyFileItem{ resp.Items = append(resp.Items, &EmbyFileItem{
Item: &model.Item{ Item: &model.Item{
Name: i.Name, Name: i.Name,
Path: i.Id, Path: fmt.Sprintf("%s/%s", aucd.ServerID, i.Id),
IsDir: i.IsFolder, IsDir: i.IsFolder,
}, },
Type: i.Type, Type: i.Type,

@ -48,9 +48,10 @@ func Login(ctx *gin.Context) {
backend := ctx.Query("backend") backend := ctx.Query("backend")
cli := vendor.LoadEmbyClient(backend) cli := vendor.LoadEmbyClient(backend)
var serverID string
if req.ApiKey != "" { if req.ApiKey != "" {
_, err := cli.GetSystemInfo(ctx, &emby.SystemInfoReq{ i, err := cli.GetSystemInfo(ctx, &emby.SystemInfoReq{
Host: req.Host, Host: req.Host,
Token: req.ApiKey, Token: req.ApiKey,
}) })
@ -58,6 +59,7 @@ func Login(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return return
} }
serverID = i.Id
} else { } else {
data, err := cli.Login(ctx, &emby.LoginReq{ data, err := cli.Login(ctx, &emby.LoginReq{
Host: req.Host, Host: req.Host,
@ -69,13 +71,20 @@ func Login(ctx *gin.Context) {
return return
} }
req.ApiKey = data.Token req.ApiKey = data.Token
serverID = data.ServerId
} }
_, err := db.CreateOrSaveEmbyVendor(user.ID, &dbModel.EmbyVendor{ if serverID == "" {
UserID: user.ID, ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("serverID is empty"))
Host: req.Host, return
ApiKey: req.ApiKey, }
Backend: backend,
_, err := db.CreateOrSaveEmbyVendor(&dbModel.EmbyVendor{
UserID: user.ID,
ServerID: serverID,
Host: req.Host,
ApiKey: req.ApiKey,
Backend: backend,
}) })
if err != nil { if err != nil {
@ -83,11 +92,12 @@ func Login(ctx *gin.Context) {
return return
} }
_, err = user.EmbyCache().Data().Refresh(ctx, func(ctx context.Context, args ...struct{}) (*cache.EmbyUserCacheData, error) { _, err = user.EmbyCache().StoreOrRefreshWithDynamicFunc(ctx, serverID, func(ctx context.Context, key string, args ...struct{}) (*cache.EmbyUserCacheData, error) {
return &cache.EmbyUserCacheData{ return &cache.EmbyUserCacheData{
Host: req.Host, Host: req.Host,
ApiKey: req.ApiKey, ServerID: key,
Backend: backend, ApiKey: req.ApiKey,
Backend: backend,
}, nil }, nil
}) })
if err != nil { if err != nil {
@ -98,19 +108,40 @@ func Login(ctx *gin.Context) {
ctx.Status(http.StatusNoContent) 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) { func Logout(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User) user := ctx.MustGet("user").(*op.User)
err := db.DeleteEmbyVendor(user.ID) var req LogoutReq
if err := model.Decode(ctx, &req); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
err := db.DeleteEmbyVendor(user.ID, req.ServerID)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
} }
eucd := user.EmbyCache().Raw() eucd, ok := user.EmbyCache().LoadCache(req.ServerID)
user.EmbyCache().Clear() if ok {
go logoutEmby(eucd.Raw())
go logoutEmby(eucd) }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }

@ -17,14 +17,15 @@ type EmbyMeResp = model.VendorMeResp[*emby.SystemInfoResp]
func Me(ctx *gin.Context) { func Me(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User) user := ctx.MustGet("user").(*op.User)
eucd, err := user.EmbyCache().Get(ctx) serverID := ctx.Query("serverID")
if serverID == "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(errors.New("serverID is required")))
return
}
eucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
if err != nil { if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&EmbyMeResp{
IsLogin: false,
}))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
} }
@ -43,3 +44,37 @@ func Me(ctx *gin.Context) {
Info: data, Info: data,
})) }))
} }
type EmbyBindsResp []*struct {
ServerID string `json:"serverID"`
Host string `json:"host"`
}
func Binds(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
ev, err := db.GetEmbyVendors(user.ID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&EmbyMeResp{
IsLogin: false,
}))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
var resp EmbyBindsResp = make(EmbyBindsResp, 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))
}

Loading…
Cancel
Save