Feat: crypto vendor info in db

This commit is contained in:
zijiren233 2023-12-10 22:04:49 +08:00
parent ae1f6c1407
commit 37094dd2bc
24 changed files with 510 additions and 237 deletions

2
go.mod
View File

@ -31,7 +31,7 @@ require (
github.com/synctv-org/vendors v0.1.1-0.20231209122754-ebad9251fa7a
github.com/ulule/limiter/v3 v3.11.2
github.com/zencoder/go-dash/v3 v3.0.3
github.com/zijiren233/gencontainer v0.0.0-20231209155516-a52fcb19fee5
github.com/zijiren233/gencontainer v0.0.0-20231210091819-97da95d7545c
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb
github.com/zijiren233/ksync v0.2.0
github.com/zijiren233/livelib v0.2.3-0.20231103145812-58de2ae7f423

2
go.sum
View File

@ -370,6 +370,8 @@ github.com/zijiren233/gencontainer v0.0.0-20231209055719-473cab2b7931 h1:13Z/zjQ
github.com/zijiren233/gencontainer v0.0.0-20231209055719-473cab2b7931/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
github.com/zijiren233/gencontainer v0.0.0-20231209155516-a52fcb19fee5 h1:OsNDmOre1xXJpRaQUeqet3yYZbkfy8bfEdsXs8PrXSE=
github.com/zijiren233/gencontainer v0.0.0-20231209155516-a52fcb19fee5/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
github.com/zijiren233/gencontainer v0.0.0-20231210091819-97da95d7545c h1:+up6wZezwJRTzEbxM7E0PRM+kHEJUk7FzifardCdaZs=
github.com/zijiren233/gencontainer v0.0.0-20231210091819-97da95d7545c/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/TbbGodHhOVHNoPk+7v/YBJACs22gKpKlatWw=
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb/go.mod h1:6TCzjDiQ8+5gWZiwsC3pnA5M0vUy2jV2Y7ciHJh729g=
github.com/zijiren233/ksync v0.2.0 h1:OyXVXbVQYFEVfWM13NApt4LMHbLQ3HTs4oYcLmqL6NE=

View File

@ -21,7 +21,16 @@ var (
func Init(d *gorm.DB, t conf.DatabaseType) error {
db = d
dbType = t
return AutoMigrate(new(model.Setting), new(model.User), new(model.UserProvider), new(model.Room), new(model.RoomUserRelation), new(model.StreamingVendorInfo), new(model.Movie))
return AutoMigrate(
new(model.Setting),
new(model.User),
new(model.UserProvider),
new(model.Room),
new(model.RoomUserRelation),
new(model.Movie),
new(model.BilibiliVendor),
new(model.AlistVendor),
)
}
func AutoMigrate(dst ...any) error {

View File

@ -3,18 +3,19 @@ package db
import (
log "github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/model"
"gorm.io/gorm"
)
type dbVersion struct {
NextVersion string
Upgrade func() error
Upgrade func(*gorm.DB) error
}
const CurrentVersion = "0.0.1"
var dbVersions = map[string]dbVersion{
"0.0.1": {
NextVersion: "",
NextVersion: "0.0.2-dev",
Upgrade: nil,
},
}
@ -43,7 +44,7 @@ func upgradeDatabase() error {
}
log.Infof("Upgrading database to version %s", currentVersion)
if version.Upgrade != nil {
err = version.Upgrade()
err = version.Upgrade(db)
if err != nil {
return err
}

View File

@ -2,88 +2,54 @@ package db
import (
"errors"
"net/http"
"github.com/synctv-org/synctv/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetVendorByUserID(userID string) ([]*model.StreamingVendorInfo, error) {
var vendors []*model.StreamingVendorInfo
err := db.Where("user_id = ?", userID).Find(&vendors).Error
if err != nil {
return nil, err
}
return vendors, nil
func GetBilibiliVendor(userID string) (*model.BilibiliVendor, error) {
var vendor model.BilibiliVendor
err := db.Where("user_id = ?", userID).Preload(clause.Associations).First(&vendor).Error
return &vendor, HandleNotFound(err, "vendor")
}
func GetVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor) (*model.StreamingVendorInfo, error) {
var vendorInfo model.StreamingVendorInfo
err := db.Where("user_id = ? AND vendor = ?", userID, vendor).First(&vendorInfo).Error
return &vendorInfo, HandleNotFound(err, "vendor")
}
type CreateVendorConfig func(*model.StreamingVendorInfo)
func WithCookie(cookie []*http.Cookie) CreateVendorConfig {
return func(vendor *model.StreamingVendorInfo) {
vendor.Cookies = cookie
}
}
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{
UserID: userID,
Vendor: vendor,
}
for _, c := range conf {
c(v)
}
err := db.Where("user_id = ? AND vendor = ?", userID, vendor).Attrs(
v,
).FirstOrCreate(&vendorInfo).Error
return &vendorInfo, err
}
func CreateOrSaveVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor, conf ...CreateVendorConfig) (*model.StreamingVendorInfo, error) {
vendorInfo := model.StreamingVendorInfo{
UserID: userID,
Vendor: vendor,
}
return &vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&vendorInfo).Error, gorm.ErrRecordNotFound) {
for _, c := range conf {
c(&vendorInfo)
}
func CreateOrSaveBilibiliVendor(userID string, vendorInfo *model.BilibiliVendor) (*model.BilibiliVendor, error) {
vendorInfo.UserID = userID
return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.BilibiliVendor{
UserID: userID,
}).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error
} else {
for _, c := range conf {
c(&vendorInfo)
}
return tx.Save(&vendorInfo).Error
}
})
}
func DeleteVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor) error {
return db.Where("user_id = ? AND vendor = ?", userID, vendor).Delete(&model.StreamingVendorInfo{}).Error
func DeleteBilibiliVendor(userID string) error {
return db.Where("user_id = ?", userID).Delete(&model.BilibiliVendor{}).Error
}
func GetAlistVendor(userID string) (*model.AlistVendor, error) {
var vendor model.AlistVendor
err := db.Where("user_id = ?", userID).Preload(clause.Associations).First(&vendor).Error
return &vendor, HandleNotFound(err, "vendor")
}
func CreateOrSaveAlistVendor(userID string, vendorInfo *model.AlistVendor) (*model.AlistVendor, error) {
vendorInfo.UserID = userID
return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.AlistVendor{
UserID: userID,
}).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error
} else {
return tx.Save(&vendorInfo).Error
}
})
}
func DeleteAlistVendor(userID string) error {
return db.Where("user_id = ?", userID).Delete(&model.AlistVendor{}).Error
}

View File

@ -42,22 +42,29 @@ type Subtitle struct {
Type string `json:"type"`
}
type VendorName = string
const (
VendorBilibili VendorName = "bilibili"
VendorAlist VendorName = "alist"
)
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"`
Vendor VendorName `json:"vendor"`
Backend string `json:"backend"`
Shared bool `gorm:"not null;default:false" json:"shared"`
Bilibili *BilibiliStreamingInfo `gorm:"embedded;embeddedPrefix:bilibili_" json:"bilibili,omitempty"`
Alist *AlistStreamingInfo `gorm:"embedded;embeddedPrefix:alist_" json:"alist,omitempty"`
}
type BilibiliVendorInfo struct {
type BilibiliStreamingInfo struct {
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 {
func (b *BilibiliStreamingInfo) Validate() error {
switch {
// 先判断epid是否为0来确定是否是pgc
case b.Epid != 0:
@ -75,7 +82,7 @@ func (b *BilibiliVendorInfo) Validate() error {
return nil
}
type AlistVendorInfo struct {
type AlistStreamingInfo struct {
Path string `json:"path,omitempty"`
Password string `json:"password,omitempty"`
}

View File

@ -42,15 +42,16 @@ type User struct {
ID string `gorm:"primaryKey;type:varchar(32)" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
RegisteredByProvider bool `gorm:"not null;default:false"`
UserProviders []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Username string `gorm:"not null;uniqueIndex"`
HashedPassword []byte `gorm:"not null"`
Role Role `gorm:"not null;default:2"`
RoomUserRelations []RoomUserRelation `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
StreamingVendorInfos []StreamingVendorInfo `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
RegisteredByProvider bool `gorm:"not null;default:false"`
UserProviders []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Username string `gorm:"not null;uniqueIndex"`
HashedPassword []byte `gorm:"not null"`
Role Role `gorm:"not null;default:2"`
RoomUserRelations []RoomUserRelation `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
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"`
}
func (u *User) CheckPassword(password string) bool {

View File

@ -1,28 +1,77 @@
package model
import (
"net/http"
"time"
"github.com/synctv-org/synctv/utils"
"gorm.io/gorm"
)
type StreamingVendor string
const (
StreamingVendorBilibili StreamingVendor = "bilibili"
StreamingVendorAlist StreamingVendor = "alist"
)
type StreamingVendorInfo struct {
UserID string `gorm:"not null;primarykey"`
Vendor StreamingVendor `gorm:"not null;primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
VendorToken
Host string
type BilibiliVendor struct {
UserID string `gorm:"primaryKey"`
Cookies map[string]string `gorm:"serializer:fastjson"`
}
type VendorToken struct {
Cookies []*http.Cookie `gorm:"serializer:fastjson"`
Authorization string
Password string
func (b *BilibiliVendor) BeforeSave(tx *gorm.DB) error {
key := []byte(b.UserID)
for k, v := range b.Cookies {
value, err := utils.CryptoToBase64([]byte(v), key)
if err != nil {
return err
}
b.Cookies[k] = value
}
return nil
}
func (b *BilibiliVendor) AfterFind(tx *gorm.DB) error {
key := []byte(b.UserID)
for k, v := range b.Cookies {
value, err := utils.DecryptoFromBase64(v, key)
if err != nil {
return err
}
b.Cookies[k] = string(value)
}
return nil
}
type AlistVendor struct {
UserID string `gorm:"primaryKey"`
Host string `gorm:"serializer:fastjson"`
Username string `gorm:"serializer:fastjson"`
Password string `gorm:"serializer:fastjson"`
}
func (a *AlistVendor) BeforeSave(tx *gorm.DB) error {
key := []byte(a.UserID)
var err error
if a.Host, err = utils.CryptoToBase64([]byte(a.Host), key); err != nil {
return err
}
if a.Username, err = utils.CryptoToBase64([]byte(a.Username), key); err != nil {
return err
}
if a.Password, err = utils.CryptoToBase64([]byte(a.Password), key); err != nil {
return err
}
return nil
}
func (a *AlistVendor) AfterFind(tx *gorm.DB) error {
key := []byte(a.UserID)
if v, err := utils.DecryptoFromBase64(a.Host, key); err != nil {
return err
} else {
a.Host = string(v)
}
if v, err := utils.DecryptoFromBase64(a.Username, key); err != nil {
return err
} else {
a.Username = string(v)
}
if v, err := utils.DecryptoFromBase64(a.Password, key); err != nil {
return err
} else {
a.Password = string(v)
}
return nil
}

110
internal/op/cache.go Normal file
View File

@ -0,0 +1,110 @@
package op
import (
"sync"
"time"
"github.com/zijiren233/gencontainer/refreshcache"
"golang.org/x/exp/maps"
)
type Cache struct {
lock sync.RWMutex
cache map[string]*refreshcache.RefreshCache[any]
}
func newBaseCache() *Cache {
return &Cache{
cache: make(map[string]*refreshcache.RefreshCache[any]),
}
}
func (b *Cache) Clear() {
b.lock.Lock()
defer b.lock.Unlock()
b.clear()
}
func (b *Cache) clear() {
maps.Clear(b.cache)
}
func (b *Cache) LoadOrStore(id string, refreshFunc func() (any, error), maxAge time.Duration) (any, error) {
b.lock.RLock()
c, loaded := b.cache[id]
if loaded {
b.lock.RUnlock()
return c.Get()
}
b.lock.RUnlock()
b.lock.Lock()
c, loaded = b.cache[id]
if loaded {
b.lock.Unlock()
return c.Get()
}
c = refreshcache.NewRefreshCache[any](refreshFunc, maxAge)
b.cache[id] = c
b.lock.Unlock()
return c.Get()
}
func (b *Cache) StoreOrRefresh(id string, refreshFunc func() (any, error), maxAge time.Duration) (any, error) {
b.lock.RLock()
c, ok := b.cache[id]
if ok {
b.lock.RUnlock()
return c.Refresh()
}
b.lock.RUnlock()
b.lock.Lock()
c, ok = b.cache[id]
if ok {
b.lock.Unlock()
return c.Refresh()
}
c = refreshcache.NewRefreshCache[any](refreshFunc, maxAge)
b.cache[id] = c
b.lock.Unlock()
return c.Refresh()
}
func (b *Cache) LoadOrStoreWithDynamicFunc(id string, refreshFunc func() (any, error), maxAge time.Duration) (any, error) {
b.lock.RLock()
c, loaded := b.cache[id]
if loaded {
b.lock.RUnlock()
return c.Data().Get(refreshFunc)
}
b.lock.RUnlock()
b.lock.Lock()
c, loaded = b.cache[id]
if loaded {
b.lock.Unlock()
return c.Data().Get(refreshFunc)
}
c = refreshcache.NewRefreshCache[any](refreshFunc, maxAge)
b.cache[id] = c
b.lock.Unlock()
return c.Data().Get(refreshFunc)
}
func (b *Cache) StoreOrRefreshWithDynamicFunc(id string, refreshFunc func() (any, error), maxAge time.Duration) (any, error) {
b.lock.RLock()
c, ok := b.cache[id]
if ok {
b.lock.RUnlock()
return c.Data().Refresh(refreshFunc)
}
b.lock.RUnlock()
b.lock.Lock()
c, ok = b.cache[id]
if ok {
b.lock.Unlock()
return c.Data().Refresh(refreshFunc)
}
c = refreshcache.NewRefreshCache[any](refreshFunc, maxAge)
b.cache[id] = c
b.lock.Unlock()
return c.Data().Refresh(refreshFunc)
}

View File

@ -12,62 +12,19 @@ import (
"github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/settings"
"github.com/synctv-org/synctv/utils"
"github.com/zijiren233/gencontainer/refreshcache"
"github.com/zijiren233/livelib/av"
"github.com/zijiren233/livelib/container/flv"
"github.com/zijiren233/livelib/protocol/hls"
rtmpProto "github.com/zijiren233/livelib/protocol/rtmp"
"github.com/zijiren233/livelib/protocol/rtmp/core"
rtmps "github.com/zijiren233/livelib/server"
"golang.org/x/exp/maps"
)
type Movie struct {
Movie model.Movie
lock *sync.RWMutex
channel *rtmps.Channel
cache *BaseCache
}
type BaseCache struct {
lock sync.RWMutex
cache map[string]*refreshcache.RefreshData[any]
}
func newBaseCache() *BaseCache {
return &BaseCache{
cache: make(map[string]*refreshcache.RefreshData[any]),
}
}
func (b *BaseCache) Clear() {
b.lock.Lock()
defer b.lock.Unlock()
b.clear()
}
func (b *BaseCache) clear() {
maps.Clear(b.cache)
}
func (b *BaseCache) LoadOrStore(id string, refreshFunc func() (any, error), maxAge time.Duration) (any, error) {
b.lock.RLock()
c, loaded := b.cache[id]
if loaded {
b.lock.RUnlock()
return c.Get(refreshFunc)
}
b.lock.RUnlock()
b.lock.Lock()
c, loaded = b.cache[id]
if loaded {
b.lock.Unlock()
return c, nil
}
c = refreshcache.NewRefreshData[any](maxAge)
b.cache[id] = c
b.lock.Unlock()
return c.Get(refreshFunc)
Cache *Cache
}
func (m *Movie) Channel() (*rtmps.Channel, error) {
@ -76,10 +33,6 @@ func (m *Movie) Channel() (*rtmps.Channel, error) {
return m.channel, m.init()
}
func (m *Movie) Cache() *BaseCache {
return m.cache
}
func genTsName() string {
return utils.SortUUID()
}
@ -225,10 +178,11 @@ func (movie *Movie) Validate() error {
func (movie *Movie) validateVendorMovie() error {
switch movie.Movie.Base.VendorInfo.Vendor {
case model.StreamingVendorBilibili:
case model.VendorBilibili:
return movie.Movie.Base.VendorInfo.Bilibili.Validate()
case model.StreamingVendorAlist:
case model.VendorAlist:
// return movie.Movie.Base.VendorInfo.Alist.Validate()
default:
return fmt.Errorf("vendor not support")
@ -248,7 +202,7 @@ func (m *Movie) terminate() {
m.channel.Close()
m.channel = nil
}
m.cache.clear()
m.Cache.clear()
}
func (m *Movie) Update(movie *model.BaseMovie) error {

View File

@ -25,7 +25,7 @@ func (m *movies) init() {
m.list.PushBack(&Movie{
Movie: *m2,
lock: new(sync.RWMutex),
cache: newBaseCache(),
Cache: newBaseCache(),
})
}
})
@ -46,7 +46,7 @@ func (m *movies) AddMovie(mo *model.Movie) error {
movie := &Movie{
Movie: *mo,
lock: new(sync.RWMutex),
cache: newBaseCache(),
Cache: newBaseCache(),
}
err := movie.init()
@ -75,7 +75,7 @@ func (m *movies) AddMovies(mos []*model.Movie) error {
movie := &Movie{
Movie: *mo,
lock: new(sync.RWMutex),
cache: newBaseCache(),
Cache: newBaseCache(),
}
err := movie.init()

View File

@ -16,6 +16,7 @@ import (
type User struct {
model.User
version uint32
Cache *Cache
}
func (u *User) Version() uint32 {
@ -69,11 +70,11 @@ func (u *User) NewMovie(movie *model.BaseMovie) (*model.Movie, error) {
return nil, errors.New("movie is nil")
}
switch movie.VendorInfo.Vendor {
case model.StreamingVendorBilibili:
case model.VendorBilibili:
if movie.VendorInfo.Bilibili == nil {
return nil, errors.New("bilibili payload is nil")
}
case model.StreamingVendorAlist:
case model.VendorAlist:
if movie.VendorInfo.Alist == nil {
return nil, errors.New("alist payload is nil")
}

View File

@ -28,6 +28,7 @@ func LoadOrInitUser(u *model.User) (*User, error) {
i, _ := userCache.LoadOrStore(u.ID, &User{
User: *u,
version: crc32.ChecksumIEEE(u.HashedPassword),
Cache: newBaseCache(),
}, time.Hour)
return i.Value(), nil
}

View File

@ -473,7 +473,7 @@ func ProxyMovie(ctx *gin.Context) {
switch m.Movie.Base.Type {
case "mpd":
mpdI, err := m.Cache().LoadOrStore("", initDashCache(ctx, &m.Movie), time.Minute*5)
mpdI, err := m.Cache.LoadOrStoreWithDynamicFunc("", initDashCache(ctx, &m.Movie), time.Minute*5)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
@ -673,13 +673,13 @@ type bilibiliCache struct {
func initBilibiliMPDCache(ctx context.Context, movie dbModel.Movie) func() (any, error) {
return func() (any, error) {
var cookies []*http.Cookie
vendorInfo, err := db.GetVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili)
vendorInfo, err := db.GetBilibiliVendor(movie.CreatorID)
if err != nil {
if !errors.Is(err, db.ErrNotFound("vendor")) {
return nil, err
}
} else {
cookies = vendorInfo.Cookies
cookies = utils.MapToHttpCookie(vendorInfo.Cookies)
}
cli := vendor.BilibiliClient(movie.Base.VendorInfo.Backend)
var m, hevcM *mpd.MPD
@ -766,13 +766,13 @@ func initBilibiliMPDCache(ctx context.Context, movie dbModel.Movie) func() (any,
func initBilibiliCache(ctx context.Context, movie dbModel.Movie, cookieUserID string) func() (any, error) {
return func() (any, error) {
var cookies []*http.Cookie
vendorInfo, err := db.GetVendorByUserIDAndVendor(cookieUserID, dbModel.StreamingVendorBilibili)
vendorInfo, err := db.GetBilibiliVendor(cookieUserID)
if err != nil {
if !errors.Is(err, db.ErrNotFound("vendor")) {
return nil, err
}
} else {
cookies = vendorInfo.Cookies
cookies = utils.MapToHttpCookie(vendorInfo.Cookies)
}
cli := vendor.BilibiliClient(movie.Base.VendorInfo.Backend)
var u string
@ -839,13 +839,13 @@ func initBilibiliSubtitleCache(ctx context.Context, movie dbModel.Movie) func()
}
var cookies []*http.Cookie
vendorInfo, err := db.GetVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili)
vendorInfo, err := db.GetBilibiliVendor(movie.CreatorID)
if err != nil {
if !errors.Is(err, db.ErrNotFound("vendor")) {
return nil, err
}
} else {
cookies = vendorInfo.Cookies
cookies = utils.MapToHttpCookie(vendorInfo.Cookies)
}
cli := vendor.BilibiliClient(movie.Base.VendorInfo.Backend)
resp, err := cli.GetSubtitles(ctx, &bilibili.GetSubtitlesReq{
@ -921,17 +921,17 @@ type alistCache struct {
func initAlistCache(ctx context.Context, movie dbModel.Movie) func() (any, error) {
return func() (any, error) {
v, err := db.GetVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorAlist)
if err != nil {
return nil, err
}
if v.Host == "" {
return nil, errors.New("not bind alist vendor")
}
// v, err := db.GetVendorByUserIDAndVendor(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,
// Host: v.Host,
// Token: v.Authorization,
Path: movie.Base.VendorInfo.Alist.Path,
Password: movie.Base.VendorInfo.Alist.Password,
})
@ -948,8 +948,8 @@ func initAlistCache(ctx context.Context, movie dbModel.Movie) func() (any, error
}
if fg.Provider == "AliyundriveOpen" {
fo, err := cli.FsOther(ctx, &alist.FsOtherReq{
Host: v.Host,
Token: v.Authorization,
// Host: v.Host,
// Token: v.Authorization,
Path: movie.Base.VendorInfo.Alist.Path,
Password: movie.Base.VendorInfo.Alist.Password,
Method: "video_preview",
@ -965,7 +965,7 @@ func initAlistCache(ctx context.Context, movie dbModel.Movie) func() (any, error
func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
switch movie.Movie.Base.VendorInfo.Vendor {
case dbModel.StreamingVendorBilibili:
case dbModel.VendorBilibili:
t := ctx.Query("t")
switch t {
case "", "hevc":
@ -974,7 +974,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
return
}
mpdI, err := movie.Cache().LoadOrStore(t, initBilibiliMPDCache(ctx, movie.Movie), time.Minute*119)
mpdI, err := movie.Cache.LoadOrStoreWithDynamicFunc(t, initBilibiliMPDCache(ctx, movie.Movie), time.Minute*119)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
@ -1020,7 +1020,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("n is empty"))
return
}
srtI, err := movie.Cache().LoadOrStore("subtitle", initBilibiliSubtitleCache(ctx, movie.Movie), time.Minute*15)
srtI, err := movie.Cache.LoadOrStoreWithDynamicFunc("subtitle", initBilibiliSubtitleCache(ctx, movie.Movie), time.Minute*15)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
@ -1058,9 +1058,9 @@ func parse2VendorMovie(ctx context.Context, userID string, movie *op.Movie) (err
}
switch movie.Movie.Base.VendorInfo.Vendor {
case dbModel.StreamingVendorBilibili:
case dbModel.VendorBilibili:
if !movie.Movie.Base.Proxy {
dataI, err := movie.Cache().LoadOrStore(userID, initBilibiliCache(ctx, movie.Movie, userID), time.Minute*119)
dataI, err := movie.Cache.LoadOrStoreWithDynamicFunc(userID, initBilibiliCache(ctx, movie.Movie, userID), time.Minute*119)
if err != nil {
return err
}
@ -1074,7 +1074,7 @@ func parse2VendorMovie(ctx context.Context, userID string, movie *op.Movie) (err
} else {
movie.Movie.Base.Type = "mpd"
}
srtI, err := movie.Cache().LoadOrStore("subtitle", initBilibiliSubtitleCache(ctx, movie.Movie), time.Minute*15)
srtI, err := movie.Cache.LoadOrStoreWithDynamicFunc("subtitle", initBilibiliSubtitleCache(ctx, movie.Movie), time.Minute*15)
if err != nil {
return err
}
@ -1093,8 +1093,8 @@ func parse2VendorMovie(ctx context.Context, userID string, movie *op.Movie) (err
}
return nil
case dbModel.StreamingVendorAlist:
dataI, err := movie.Cache().LoadOrStore("", initAlistCache(ctx, movie.Movie), time.Minute*15)
case dbModel.VendorAlist:
dataI, err := movie.Cache.LoadOrStoreWithDynamicFunc("", initAlistCache(ctx, movie.Movie), time.Minute*15)
if err != nil {
return err
}

View File

@ -1,8 +1,10 @@
package vendorAlist
import (
"context"
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
@ -31,6 +33,48 @@ func (r *LoginReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(r)
}
type alistCache struct {
Host string
Token string
}
func initAlistAuthorizationCacheWithConfig(ctx context.Context, cli alist.AlistHTTPServer, host, username, password string) func() (any, error) {
return func() (any, error) {
if username == "" {
_, err := cli.Me(ctx, &alist.MeReq{
Host: host,
})
return &alistCache{
Host: host,
}, err
} else {
resp, err := cli.Login(ctx, &alist.LoginReq{
Host: host,
Username: username,
Password: password,
})
if err != nil {
return nil, err
}
return &alistCache{
Host: host,
Token: resp.Token,
}, nil
}
}
}
func initAlistAuthorizationCacheWithUserID(ctx context.Context, cli alist.AlistHTTPServer, userID string) func() (any, error) {
return func() (any, error) {
v, err := db.GetAlistVendor(userID)
if err != nil {
return nil, err
}
return initAlistAuthorizationCacheWithConfig(ctx, cli, v.Host, v.Username, v.Password)()
}
}
func Login(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
@ -42,20 +86,26 @@ func Login(ctx *gin.Context) {
cli := vendor.AlistClient("")
var (
authI any
err error
)
if req.Username == "" {
_, err := cli.Me(ctx, &alist.MeReq{
_, err = cli.Me(ctx, &alist.MeReq{
Host: req.Host,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
}
_, err = db.CreateOrSaveVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist, db.WithHost(req.Host), db.WithAuthorization(""))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
authI, err = user.Cache.StoreOrRefreshWithDynamicFunc("alist_authorization", func() (any, error) {
return &alistCache{
Host: req.Host,
}, nil
}, time.Hour*24)
} else {
resp, err := cli.Login(ctx, &alist.LoginReq{
var resp *alist.LoginResp
resp, err = cli.Login(ctx, &alist.LoginReq{
Host: req.Host,
Username: req.Username,
Password: req.Password,
@ -65,11 +115,32 @@ func Login(ctx *gin.Context) {
return
}
_, err = db.CreateOrSaveVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist, db.WithAuthorization(resp.Token), db.WithHost(req.Host))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
authI, err = user.Cache.StoreOrRefreshWithDynamicFunc("alist_authorization", func() (any, error) {
return &alistCache{
Host: req.Host,
Token: resp.Token,
}, nil
}, time.Hour*24)
}
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
_, ok := authI.(*alistCache)
if !ok {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
_, err = db.CreateOrSaveAlistVendor(user.ID, &dbModel.AlistVendor{
Host: req.Host,
Username: req.Username,
Password: req.Password,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
ctx.Status(http.StatusNoContent)

View File

@ -3,10 +3,10 @@ package vendorAlist
import (
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"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"
@ -17,7 +17,10 @@ type AlistMeResp = model.VendorMeResp[*alist.MeResp]
func Me(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
v, err := db.GetVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist)
cli := vendor.AlistClient(ctx.Query("backend"))
authorizationI, err := user.Cache.LoadOrStoreWithDynamicFunc("alist_authorization", initAlistAuthorizationCacheWithUserID(ctx, cli, user.ID), time.Hour*24)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&AlistMeResp{
@ -28,10 +31,15 @@ func Me(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
cache, ok := authorizationI.(*alistCache)
if !ok {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
resp, err := vendor.AlistClient("").Me(ctx, &alist.MeReq{
Host: v.Host,
Token: v.Authorization,
resp, err := cli.Me(ctx, &alist.MeReq{
Host: cache.Host,
Token: cache.Token,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
@ -42,4 +50,5 @@ func Me(ctx *gin.Context) {
IsLogin: false,
Info: resp,
}))
}

View File

@ -1,13 +1,13 @@
package vendorAlist
import (
"fmt"
"errors"
"net/http"
"time"
"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"
@ -40,21 +40,30 @@ func List(ctx *gin.Context) {
return
}
v, err := db.GetVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorAlist)
var cli = vendor.AlistClient(ctx.Query("backend"))
cacheI, err := user.Cache.LoadOrStoreWithDynamicFunc("alist_authorization", initAlistAuthorizationCacheWithUserID(ctx, cli, user.ID), time.Hour*24)
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
}
cache, ok := cacheI.(*alistCache)
if !ok {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
fmt.Printf("v.Authorization: %v\n", v.Authorization)
var cli = vendor.AlistClient(ctx.Query("backend"))
resp, err := cli.FsList(ctx, &alist.FsListReq{
Token: v.Authorization,
Token: cache.Token,
Password: req.Password,
Path: req.Path,
Host: v.Host,
Host: cache.Host,
Refresh: req.Refresh,
})
if err != nil {

View File

@ -8,7 +8,6 @@ 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"
@ -51,14 +50,14 @@ func Parse(ctx *gin.Context) {
}
var cookies []*http.Cookie
vendorInfo, err := db.GetVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili)
vendorInfo, err := db.GetBilibiliVendor(user.ID)
if err != nil {
if !errors.Is(err, db.ErrNotFound("vendor")) {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
} else {
cookies = vendorInfo.Cookies
cookies = utils.MapToHttpCookie(vendorInfo.Cookies)
}
switch resp.Type {

View File

@ -11,7 +11,6 @@ import (
"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/bilibili"
)
@ -73,7 +72,9 @@ func LoginWithQR(ctx *gin.Context) {
}))
return
case bilibili.QRCodeStatus_SUCCESS:
_, err = db.CreateOrSaveVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie(utils.MapToHttpCookie(resp.Cookies)))
_, err = db.CreateOrSaveBilibiliVendor(user.ID, &dbModel.BilibiliVendor{
Cookies: resp.Cookies,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
@ -185,7 +186,9 @@ func LoginWithSMS(ctx *gin.Context) {
return
}
user := ctx.MustGet("user").(*op.User)
_, err = db.CreateOrSaveVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie(utils.MapToHttpCookie(c.Cookies)))
_, err = db.CreateOrSaveBilibiliVendor(user.ID, &dbModel.BilibiliVendor{
Cookies: c.Cookies,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
@ -195,7 +198,7 @@ func LoginWithSMS(ctx *gin.Context) {
func Logout(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
err := db.DeleteVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili)
err := db.DeleteBilibiliVendor(user.ID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return

View File

@ -6,11 +6,9 @@ import (
"github.com/gin-gonic/gin"
"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/bilibili"
)
@ -18,7 +16,7 @@ type BilibiliMeResp = model.VendorMeResp[*bilibili.UserInfoResp]
func Me(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
v, err := db.GetVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili)
v, err := db.GetBilibiliVendor(user.ID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&BilibiliMeResp{
@ -36,7 +34,7 @@ func Me(ctx *gin.Context) {
return
}
resp, err := vendor.BilibiliClient("").UserInfo(ctx, &bilibili.UserInfoReq{
Cookies: utils.HttpCookieToMap(v.Cookies),
Cookies: v.Cookies,
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))

View File

@ -12,10 +12,10 @@ import (
func Backends(ctx *gin.Context) {
var backends []string
switch dbModel.StreamingVendor(ctx.Param("vendor")) {
case dbModel.StreamingVendorBilibili:
switch ctx.Param("vendor") {
case dbModel.VendorBilibili:
backends = maps.Keys(vendor.BilibiliClients())
case dbModel.StreamingVendorAlist:
case dbModel.VendorAlist:
backends = maps.Keys(vendor.AlistClients())
default:
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid vendor"))

62
utils/crypto.go Normal file
View File

@ -0,0 +1,62 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
func Crypto(v []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(v))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], v)
return ciphertext, nil
}
func Decrypto(v []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(v) < aes.BlockSize {
return nil, errors.New("ciphertext too short")
}
iv := v[:aes.BlockSize]
v = v[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(v, v)
return v, nil
}
func CryptoToBase64(v []byte, key []byte) (string, error) {
ciphertext, err := Crypto(v, key)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func DecryptoFromBase64(v string, key []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return nil, err
}
return Decrypto(ciphertext, key)
}

22
utils/crypto_test.go Normal file
View File

@ -0,0 +1,22 @@
package utils_test
import (
"testing"
"github.com/synctv-org/synctv/utils"
)
func TestCrypto(t *testing.T) {
m := []byte("hello world")
key := []byte(utils.RandString(32))
m, err := utils.Crypto(m, key)
if err != nil {
t.Fatal(err)
}
t.Log(string(m))
m, err = utils.Decrypto(m, key)
if err != nil {
t.Fatal(err)
}
t.Log(string(m))
}

View File

@ -14,7 +14,7 @@ var json = jsoniter.ConfigCompatibleWithStandardLibrary
type JSONSerializer struct{}
func (*JSONSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
func (*JSONSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue any) (err error) {
fieldValue := reflect.New(field.FieldType)
if dbValue != nil {
@ -35,8 +35,7 @@ func (*JSONSerializer) Scan(ctx context.Context, field *schema.Field, dst reflec
return
}
// 实现 Value 方法
func (*JSONSerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) {
func (*JSONSerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue any) (any, error) {
return json.Marshal(fieldValue)
}