diff --git a/cmd/admin/add.go b/cmd/admin/add.go index 77235d1..e4f0e7d 100644 --- a/cmd/admin/add.go +++ b/cmd/admin/add.go @@ -3,7 +3,6 @@ package admin import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var AddCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s", err) return nil diff --git a/cmd/admin/delete.go b/cmd/admin/delete.go index 034420f..d26dd30 100644 --- a/cmd/admin/delete.go +++ b/cmd/admin/delete.go @@ -3,7 +3,6 @@ package admin import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var RemoveCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s", err) return nil diff --git a/cmd/admin/show.go b/cmd/admin/show.go index ae294ea..2f34fe6 100644 --- a/cmd/admin/show.go +++ b/cmd/admin/show.go @@ -22,7 +22,7 @@ var ShowCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { admins := db.GetAdmins() for _, admin := range admins { - fmt.Printf("id: %d\tusername: %s\n", admin.ID, admin.Username) + fmt.Printf("id: %s\tusername: %s\n", admin.ID, admin.Username) } return nil }, diff --git a/cmd/root/add.go b/cmd/root/add.go index ff24fac..542ac95 100644 --- a/cmd/root/add.go +++ b/cmd/root/add.go @@ -3,7 +3,6 @@ package root import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var AddCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s", err) return nil diff --git a/cmd/root/delete.go b/cmd/root/delete.go index 7a068b7..54fc241 100644 --- a/cmd/root/delete.go +++ b/cmd/root/delete.go @@ -3,7 +3,6 @@ package root import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var RemoveCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s", err) return nil diff --git a/cmd/root/show.go b/cmd/root/show.go index b943f5b..1649914 100644 --- a/cmd/root/show.go +++ b/cmd/root/show.go @@ -22,7 +22,7 @@ var ShowCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { roots := db.GetRoots() for _, root := range roots { - fmt.Printf("id: %d\tusername: %s\n", root.ID, root.Username) + fmt.Printf("id: %s\tusername: %s\n", root.ID, root.Username) } return nil }, diff --git a/cmd/user/ban.go b/cmd/user/ban.go index 22f34c0..f9eacb1 100644 --- a/cmd/user/ban.go +++ b/cmd/user/ban.go @@ -3,7 +3,6 @@ package user import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var BanCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s\n", err) return nil diff --git a/cmd/user/delete.go b/cmd/user/delete.go index d71d7ae..5d59b40 100644 --- a/cmd/user/delete.go +++ b/cmd/user/delete.go @@ -3,7 +3,6 @@ package user import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var DeleteCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.LoadAndDeleteUserByID(uint(id)) + u, err := db.LoadAndDeleteUserByID(args[0]) if err != nil { fmt.Printf("delete user failed: %s\n", err) return nil diff --git a/cmd/user/search.go b/cmd/user/search.go index dc9a41c..dc66d1d 100644 --- a/cmd/user/search.go +++ b/cmd/user/search.go @@ -33,7 +33,7 @@ var SearchCmd = &cobra.Command{ return nil } for _, u := range us { - fmt.Printf("id: %d\tusername: %s\tcreated_at: %s\trole: %s\n", u.ID, u.Username, u.CreatedAt, u.Role) + fmt.Printf("id: %s\tusername: %s\tcreated_at: %s\trole: %s\n", u.ID, u.Username, u.CreatedAt, u.Role) } return nil }, diff --git a/cmd/user/unban.go b/cmd/user/unban.go index 8250a55..0e09e4b 100644 --- a/cmd/user/unban.go +++ b/cmd/user/unban.go @@ -3,7 +3,6 @@ package user import ( "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/synctv-org/synctv/internal/bootstrap" @@ -25,11 +24,7 @@ var UnbanCmd = &cobra.Command{ if len(args) == 0 { return errors.New("missing user id") } - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid user id: %s", args[0]) - } - u, err := db.GetUserByID(uint(id)) + u, err := db.GetUserByID(args[0]) if err != nil { fmt.Printf("get user failed: %s\n", err) return nil diff --git a/go.mod b/go.mod index 5f3cf87..4fe02d5 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/maruel/natural v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index e57566a..7fd5e05 100644 --- a/go.sum +++ b/go.sum @@ -123,11 +123,13 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -249,6 +251,7 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/bootstrap/rtmp.go b/internal/bootstrap/rtmp.go index 4d25c29..4694119 100644 --- a/internal/bootstrap/rtmp.go +++ b/internal/bootstrap/rtmp.go @@ -3,7 +3,6 @@ package bootstrap import ( "context" "fmt" - "strconv" log "github.com/sirupsen/logrus" "github.com/synctv-org/synctv/internal/conf" @@ -27,12 +26,7 @@ func auth(ReqAppName, ReqChannelName string, IsPublisher bool) (*rtmps.Channel, return nil, err } log.Infof("rtmp: publisher login success: %s/%s", ReqAppName, channelName) - id, err := strconv.Atoi(ReqAppName) - if err != nil { - log.Errorf("rtmp: parse channel name to id error: %v", err) - return nil, err - } - r, err := op.LoadOrInitRoomByID(uint(id)) + r, err := op.LoadOrInitRoomByID(ReqAppName) if err != nil { log.Errorf("rtmp: get room by id error: %v", err) return nil, err @@ -44,12 +38,7 @@ func auth(ReqAppName, ReqChannelName string, IsPublisher bool) (*rtmps.Channel, log.Warnf("rtmp: dial to %s/%s error: %s", ReqAppName, ReqChannelName, "rtmp player is not enabled") return nil, fmt.Errorf("rtmp: dial to %s/%s error: %s", ReqAppName, ReqChannelName, "rtmp player is not enabled") } - id, err := strconv.Atoi(ReqAppName) - if err != nil { - log.Errorf("rtmp: parse channel name to id error: %v", err) - return nil, err - } - r, err := op.LoadOrInitRoomByID(uint(id)) + r, err := op.LoadOrInitRoomByID(ReqAppName) if err != nil { log.Errorf("rtmp: get room by id error: %v", err) return nil, err diff --git a/internal/db/db.go b/internal/db/db.go index 0373c4f..ff62249 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -19,7 +19,7 @@ var ( func Init(d *gorm.DB, t conf.DatabaseType) error { db = d dbType = t - return AutoMigrate(new(model.Movie), new(model.Room), new(model.User), new(model.RoomUserRelation), new(model.UserProvider), new(model.Setting)) + return AutoMigrate(new(model.Movie), new(model.Room), new(model.User), new(model.RoomUserRelation), new(model.UserProvider), new(model.Setting), new(model.StreamingVendorInfo)) } func AutoMigrate(dst ...any) error { @@ -103,19 +103,19 @@ func WithUserAndProvider(db *gorm.DB) *gorm.DB { return db.Preload("User").Preload("User.Provider") } -func WhereRoomID(roomID uint) func(db *gorm.DB) *gorm.DB { +func WhereRoomID(roomID string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("room_id = ?", roomID) } } -func WhereUserID(userID uint) func(db *gorm.DB) *gorm.DB { +func WhereUserID(userID string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("user_id = ?", userID) } } -func WhereCreatorID(creatorID uint) func(db *gorm.DB) *gorm.DB { +func WhereCreatorID(creatorID string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("creator_id = ?", creatorID) } @@ -140,7 +140,7 @@ func WhereLike(column string, value string) func(db *gorm.DB) *gorm.DB { } } -func WhereRoomNameLikeOrCreatorIn(name string, ids []uint) func(db *gorm.DB) *gorm.DB { +func WhereRoomNameLikeOrCreatorIn(name string, ids []string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { switch dbType { case conf.DatabaseTypePostgres: @@ -173,7 +173,7 @@ func WhereUserNameLike(name string) func(db *gorm.DB) *gorm.DB { } } -func WhereCreatorIDIn(ids []uint) func(db *gorm.DB) *gorm.DB { +func WhereCreatorIDIn(ids []string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("creator_id IN ?", ids) } diff --git a/internal/db/movie.go b/internal/db/movie.go index cd64e4b..adb7c19 100644 --- a/internal/db/movie.go +++ b/internal/db/movie.go @@ -13,13 +13,13 @@ func CreateMovie(movie *model.Movie) error { return db.Create(movie).Error } -func GetAllMoviesByRoomID(roomID uint) []*model.Movie { +func GetAllMoviesByRoomID(roomID string) []*model.Movie { movies := []*model.Movie{} db.Where("room_id = ?", roomID).Order("position ASC").Find(&movies) return movies } -func DeleteMovieByID(roomID, id uint) error { +func DeleteMovieByID(roomID, id string) error { err := db.Unscoped().Where("room_id = ? AND id = ?", roomID, id).Delete(&model.Movie{}).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or movie not found") @@ -27,7 +27,7 @@ func DeleteMovieByID(roomID, id uint) error { return err } -func LoadAndDeleteMovieByID(roomID, id uint, columns ...clause.Column) (*model.Movie, error) { +func LoadAndDeleteMovieByID(roomID, id string, columns ...clause.Column) (*model.Movie, error) { movie := &model.Movie{} err := db.Unscoped().Clauses(clause.Returning{Columns: columns}).Where("room_id = ? AND id = ?", roomID, id).Delete(movie).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -36,7 +36,7 @@ func LoadAndDeleteMovieByID(roomID, id uint, columns ...clause.Column) (*model.M return movie, err } -func DeleteMoviesByRoomID(roomID uint) error { +func DeleteMoviesByRoomID(roomID string) error { err := db.Unscoped().Where("room_id = ?", roomID).Delete(&model.Movie{}).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room not found") @@ -44,7 +44,7 @@ func DeleteMoviesByRoomID(roomID uint) error { return err } -func LoadAndDeleteMoviesByRoomID(roomID uint, columns ...clause.Column) ([]*model.Movie, error) { +func LoadAndDeleteMoviesByRoomID(roomID string, columns ...clause.Column) ([]*model.Movie, error) { movies := []*model.Movie{} err := db.Unscoped().Clauses(clause.Returning{Columns: columns}).Where("room_id = ?", roomID).Delete(&movies).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -69,7 +69,7 @@ func SaveMovie(movie *model.Movie, columns ...clause.Column) error { return err } -func SwapMoviePositions(roomID uint, movie1ID uint, movie2ID uint) (err error) { +func SwapMoviePositions(roomID, movie1ID, movie2ID string) (err error) { tx := db.Begin() defer func() { if err != nil { @@ -83,14 +83,14 @@ func SwapMoviePositions(roomID uint, movie1ID uint, movie2ID uint) (err error) { err = tx.Select("position").Where("room_id = ? AND id = ?", roomID, movie1ID).First(movie1).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - err = fmt.Errorf("movie with id %d not found", movie1ID) + err = fmt.Errorf("movie with id %s not found", movie1ID) } return } err = tx.Select("position").Where("room_id = ? AND id = ?", roomID, movie2ID).First(movie2).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - err = fmt.Errorf("movie with id %d not found", movie2ID) + err = fmt.Errorf("movie with id %s not found", movie2ID) } return } diff --git a/internal/db/relation.go b/internal/db/relation.go index f562a2d..cc89be1 100644 --- a/internal/db/relation.go +++ b/internal/db/relation.go @@ -7,7 +7,7 @@ import ( "gorm.io/gorm" ) -func GetRoomUserRelation(roomID, userID uint) (*model.RoomUserRelation, error) { +func GetRoomUserRelation(roomID, userID string) (*model.RoomUserRelation, error) { roomUserRelation := &model.RoomUserRelation{} err := db.Where("room_id = ? AND user_id = ?", roomID, userID).Attrs(&model.RoomUserRelation{ RoomID: roomID, @@ -21,7 +21,7 @@ func GetRoomUserRelation(roomID, userID uint) (*model.RoomUserRelation, error) { return roomUserRelation, err } -func CreateRoomUserRelation(roomID, userID uint, role model.RoomRole, permissions model.Permission) (*model.RoomUserRelation, error) { +func CreateRoomUserRelation(roomID, userID string, role model.RoomRole, permissions model.Permission) (*model.RoomUserRelation, error) { roomUserRelation := &model.RoomUserRelation{ RoomID: roomID, UserID: userID, @@ -32,7 +32,7 @@ func CreateRoomUserRelation(roomID, userID uint, role model.RoomRole, permission return roomUserRelation, err } -func SetUserRole(roomID uint, userID uint, role model.RoomRole) error { +func SetUserRole(roomID string, userID string, role model.RoomRole) error { err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("role", role).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") @@ -40,7 +40,7 @@ func SetUserRole(roomID uint, userID uint, role model.RoomRole) error { return err } -func SetUserPermission(roomID uint, userID uint, permission model.Permission) error { +func SetUserPermission(roomID string, userID string, permission model.Permission) error { err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", permission).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") @@ -48,7 +48,7 @@ func SetUserPermission(roomID uint, userID uint, permission model.Permission) er return err } -func AddUserPermission(roomID uint, userID uint, permission model.Permission) error { +func AddUserPermission(roomID string, userID string, permission model.Permission) error { err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions | ?", permission)).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") @@ -56,7 +56,7 @@ func AddUserPermission(roomID uint, userID uint, permission model.Permission) er return err } -func RemoveUserPermission(roomID uint, userID uint, permission model.Permission) error { +func RemoveUserPermission(roomID string, userID string, permission model.Permission) error { err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions & ?", ^permission)).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") @@ -64,7 +64,7 @@ func RemoveUserPermission(roomID uint, userID uint, permission model.Permission) return err } -func DeleteUserPermission(roomID uint, userID uint) error { +func DeleteUserPermission(roomID string, userID string) error { err := db.Unscoped().Where("room_id = ? AND user_id = ?", roomID, userID).Delete(&model.RoomUserRelation{}).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") diff --git a/internal/db/room.go b/internal/db/room.go index 7561d3c..d034a2a 100644 --- a/internal/db/room.go +++ b/internal/db/room.go @@ -59,7 +59,10 @@ func CreateRoom(name, password string, conf ...CreateRoomConfig) (*model.Room, e return r, err } -func GetRoomByID(id uint) (*model.Room, error) { +func GetRoomByID(id string) (*model.Room, error) { + if len(id) != 36 { + return nil, errors.New("room id is not 32 bit") + } r := &model.Room{} err := db.Where("id = ?", id).First(r).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -68,7 +71,7 @@ func GetRoomByID(id uint) (*model.Room, error) { return r, err } -func GetRoomAndCreatorByID(id uint) (*model.Room, error) { +func GetRoomAndCreatorByID(id string) (*model.Room, error) { r := &model.Room{} err := db.Preload("Creator").Where("id = ?", id).First(r).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -77,7 +80,7 @@ func GetRoomAndCreatorByID(id uint) (*model.Room, error) { return r, err } -func ChangeRoomSetting(roomID uint, setting model.Settings) error { +func ChangeRoomSetting(roomID string, setting model.Settings) error { err := db.Model(&model.Room{}).Where("id = ?", roomID).Update("setting", setting).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room not found") @@ -85,7 +88,7 @@ func ChangeRoomSetting(roomID uint, setting model.Settings) error { return err } -func ChangeUserPermission(roomID uint, userID uint, permission model.Permission) error { +func ChangeUserPermission(roomID, userID string, permission model.Permission) error { err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", permission).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room or user not found") @@ -93,7 +96,7 @@ func ChangeUserPermission(roomID uint, userID uint, permission model.Permission) return err } -func HasPermission(roomID uint, userID uint, permission model.Permission) (bool, error) { +func HasPermission(roomID, userID string, permission model.Permission) (bool, error) { ur := &model.RoomUserRelation{} err := db.Where("room_id = ? AND user_id = ?", roomID, userID).First(ur).Error if err != nil { @@ -105,7 +108,7 @@ func HasPermission(roomID uint, userID uint, permission model.Permission) (bool, return ur.Permissions.Has(permission), nil } -func DeleteRoomByID(roomID uint) error { +func DeleteRoomByID(roomID string) error { err := db.Unscoped().Delete(&model.Room{}, roomID).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room not found") @@ -113,7 +116,7 @@ func DeleteRoomByID(roomID uint) error { return err } -func HasRoom(roomID uint) (bool, error) { +func HasRoom(roomID string) (bool, error) { r := &model.Room{} err := db.Where("id = ?", roomID).First(r).Error if err != nil { @@ -137,7 +140,7 @@ func HasRoomByName(name string) (bool, error) { return true, nil } -func SetRoomPassword(roomID uint, password string) error { +func SetRoomPassword(roomID, password string) error { var hashedPassword []byte if password != "" { var err error @@ -149,7 +152,7 @@ func SetRoomPassword(roomID uint, password string) error { return SetRoomHashedPassword(roomID, hashedPassword) } -func SetRoomHashedPassword(roomID uint, hashedPassword []byte) error { +func SetRoomHashedPassword(roomID string, hashedPassword []byte) error { err := db.Model(&model.Room{}).Where("id = ?", roomID).Update("hashed_password", hashedPassword).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("room not found") @@ -181,7 +184,7 @@ func GetAllRoomsAndCreator(scopes ...func(*gorm.DB) *gorm.DB) []*model.Room { return rooms } -func GetAllRoomsByUserID(userID uint) []*model.Room { +func GetAllRoomsByUserID(userID string) []*model.Room { rooms := []*model.Room{} db.Where("creator_id = ?", userID).Find(&rooms) return rooms diff --git a/internal/db/user.go b/internal/db/user.go index aa1f26e..10a01fc 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -59,19 +59,19 @@ func CreateOrLoadUser(username string, p provider.OAuth2Provider, puid uint, con return &user, nil } -func GetProviderUserID(p provider.OAuth2Provider, puid uint) (uint, error) { +func GetProviderUserID(p provider.OAuth2Provider, puid uint) (string, error) { var userProvider model.UserProvider if err := db.Where("provider = ? AND provider_user_id = ?", p, puid).First(&userProvider).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, errors.New("user not found") + return "", errors.New("user not found") } else { - return 0, err + return "", err } } return userProvider.UserID, nil } -func AddUserToRoom(userID uint, roomID uint, role model.RoomRole, permission model.Permission) error { +func AddUserToRoom(userID, roomID string, role model.RoomRole, permission model.Permission) error { ur := &model.RoomUserRelation{ UserID: userID, RoomID: roomID, @@ -100,8 +100,8 @@ func GetUserByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) [ return users } -func GerUsersIDByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) []uint { - var ids []uint +func GerUsersIDByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) []string { + var ids []string db.Model(&model.User{}).Where(`username LIKE ?`, fmt.Sprintf("%%%s%%", username)).Scopes(scopes...).Pluck("id", &ids) return ids } @@ -115,7 +115,10 @@ func GetUserByIDOrUsernameLike(idOrUsername string, scopes ...func(*gorm.DB) *go return users, err } -func GetUserByID(id uint) (*model.User, error) { +func GetUserByID(id string) (*model.User, error) { + if len(id) != 36 { + return nil, errors.New("user id is not 32 bit") + } u := &model.User{} err := db.Where("id = ?", id).First(u).Error if errors.Is(err, gorm.ErrRecordNotFound) { @@ -124,7 +127,7 @@ func GetUserByID(id uint) (*model.User, error) { return u, err } -func GetUsersByRoomID(roomID uint, scopes ...func(*gorm.DB) *gorm.DB) ([]model.User, error) { +func GetUsersByRoomID(roomID string, scopes ...func(*gorm.DB) *gorm.DB) ([]model.User, error) { users := []model.User{} err := db.Model(&model.RoomUserRelation{}).Where("room_id = ?", roomID).Scopes(scopes...).Find(&users).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -141,7 +144,7 @@ func BanUser(u *model.User) error { return SaveUser(u) } -func BanUserByID(userID uint) error { +func BanUserByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleBanned).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -157,7 +160,7 @@ func UnbanUser(u *model.User) error { return SaveUser(u) } -func UnbanUserByID(userID uint) error { +func UnbanUserByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleUser).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -165,7 +168,7 @@ func UnbanUserByID(userID uint) error { return err } -func DeleteUserByID(userID uint) error { +func DeleteUserByID(userID string) error { err := db.Unscoped().Delete(&model.User{}, userID).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -173,7 +176,7 @@ func DeleteUserByID(userID uint) error { return err } -func LoadAndDeleteUserByID(userID uint, columns ...clause.Column) (*model.User, error) { +func LoadAndDeleteUserByID(userID string, columns ...clause.Column) (*model.User, error) { u := &model.User{} if db.Unscoped(). Clauses(clause.Returning{Columns: columns}). @@ -210,7 +213,7 @@ func GetAdmins() []*model.User { return users } -func AddAdminByID(userID uint) error { +func AddAdminByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleAdmin).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -218,7 +221,7 @@ func AddAdminByID(userID uint) error { return err } -func RemoveAdminByID(userID uint) error { +func RemoveAdminByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleUser).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -242,7 +245,7 @@ func RemoveRoot(u *model.User) error { return SaveUser(u) } -func AddRootByID(userID uint) error { +func AddRootByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleRoot).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -250,7 +253,7 @@ func AddRootByID(userID uint) error { return err } -func RemoveRootByID(userID uint) error { +func RemoveRootByID(userID string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleUser).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -269,7 +272,7 @@ func SetRole(u *model.User, role model.Role) error { return SaveUser(u) } -func SetRoleByID(userID uint, role model.Role) error { +func SetRoleByID(userID string, role model.Role) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", role).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") @@ -289,7 +292,7 @@ func GetAllUserCountWithRole(role model.Role, scopes ...func(*gorm.DB) *gorm.DB) return count } -func SetUsernameByID(userID uint, username string) error { +func SetUsernameByID(userID string, username string) error { err := db.Model(&model.User{}).Where("id = ?", userID).Update("username", username).Error if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("user not found") diff --git a/internal/db/vendor.go b/internal/db/vendor.go new file mode 100644 index 0000000..91725eb --- /dev/null +++ b/internal/db/vendor.go @@ -0,0 +1,65 @@ +package db + +import ( + "errors" + "net/http" + + "github.com/synctv-org/synctv/internal/model" + "gorm.io/gorm" +) + +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 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 + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("vendor not found") + } + return &vendorInfo, err +} + +type CreateVendorConfig func(*model.StreamingVendorInfo) + +func WithCookie(cookie []*http.Cookie) CreateVendorConfig { + return func(vendor *model.StreamingVendorInfo) { + vendor.Cookies = cookie + } +} + +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 AssignFirstOrCreateVendorByUserIDAndVendor(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).Assign( + v, + ).FirstOrCreate(&vendorInfo).Error + return &vendorInfo, err +} diff --git a/internal/model/model_test.go b/internal/model/model_test.go index 6d5356f..07f4885 100644 --- a/internal/model/model_test.go +++ b/internal/model/model_test.go @@ -56,8 +56,8 @@ func TestAddUserToRoom(t *testing.T) { t.Fatal(err) } ur := model.RoomUserRelation{ - UserID: 1, - RoomID: 1, + UserID: "1", + RoomID: "1", Role: model.RoomRoleUser, Permissions: model.DefaultPermissions, } diff --git a/internal/model/movie.go b/internal/model/movie.go index 973cb64..52e0f80 100644 --- a/internal/model/movie.go +++ b/internal/model/movie.go @@ -1,23 +1,30 @@ package model -import "time" +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) type Movie struct { - ID uint `gorm:"primarykey" json:"id"` + ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` Position uint `gorm:"not null" json:"-"` - RoomID uint `gorm:"not null;index" json:"-"` - CreatorID uint `gorm:"not null;index" json:"creatorId"` - MovieInfo + RoomID string `gorm:"not null;index" json:"-"` + CreatorID string `gorm:"not null;index" json:"creatorId"` + Base BaseMovie `gorm:"embedded;embeddedPrefix:base_" json:"base"` } -type MovieInfo struct { - Base BaseMovieInfo `gorm:"embedded;embeddedPrefix:base_" json:"base"` - PullKey string `json:"pullKey"` +func (m *Movie) BeforeCreate(tx *gorm.DB) error { + if m.ID == "" { + m.ID = uuid.NewString() + } + return nil } -type BaseMovieInfo struct { +type BaseMovie struct { Url string `json:"url"` Name string `gorm:"not null" json:"name"` Live bool `json:"live"` @@ -25,4 +32,10 @@ type BaseMovieInfo struct { RtmpSource bool `json:"rtmpSource"` Type string `json:"type"` Headers map[string]string `gorm:"serializer:fastjson" json:"headers"` + VendorInfo `gorm:"embedded;embeddedPrefix:vendor_info_" json:"vendorInfo"` +} + +type VendorInfo struct { + Vendor StreamingVendor `json:"vendor"` + Info map[string]any `gorm:"serializer:fastjson" json:"info"` } diff --git a/internal/model/oauth2.go b/internal/model/oauth2.go index fe0b6cb..005ca33 100644 --- a/internal/model/oauth2.go +++ b/internal/model/oauth2.go @@ -7,9 +7,9 @@ import ( ) type UserProvider struct { - Provider provider.OAuth2Provider `gorm:"primarykey"` - ProviderUserID uint `gorm:"primarykey;autoIncrement:false"` + Provider provider.OAuth2Provider `gorm:"not null;primarykey"` + ProviderUserID uint `gorm:"not null;primarykey;autoIncrement:false"` CreatedAt time.Time UpdatedAt time.Time - UserID uint `gorm:"not null"` + UserID string `gorm:"not null;index"` } diff --git a/internal/model/relation.go b/internal/model/relation.go index 975e99a..93709ec 100644 --- a/internal/model/relation.go +++ b/internal/model/relation.go @@ -38,11 +38,10 @@ func (p Permission) Has(permission Permission) bool { } type RoomUserRelation struct { - ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time - UserID uint `gorm:"not null;uniqueIndex:idx_user_room"` - RoomID uint `gorm:"not null;uniqueIndex:idx_user_room"` + UserID string `gorm:"not null;primarykey"` + RoomID string `gorm:"not null;primarykey"` Role RoomRole `gorm:"not null"` Permissions Permission } diff --git a/internal/model/room.go b/internal/model/room.go index 94046f7..86dba67 100644 --- a/internal/model/room.go +++ b/internal/model/room.go @@ -3,22 +3,31 @@ package model import ( "time" + "github.com/google/uuid" "github.com/zijiren233/stream" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" ) type Room struct { - ID uint `gorm:"primarykey"` + ID string `gorm:"not null;primaryKey;type:varchar(36)" json:"id"` CreatedAt time.Time UpdatedAt time.Time Name string `gorm:"not null;uniqueIndex"` Settings Settings `gorm:"embedded;embeddedPrefix:settings_"` - CreatorID uint `gorm:"index"` + CreatorID string `gorm:"index"` HashedPassword []byte GroupUserRelations []RoomUserRelation `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Movies []Movie `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } +func (r *Room) BeforeCreate(tx *gorm.DB) error { + if r.ID == "" { + r.ID = uuid.NewString() + } + return nil +} + type Settings struct { Hidden bool `json:"hidden"` } diff --git a/internal/model/user.go b/internal/model/user.go index 94c4541..4b9c95f 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -5,6 +5,7 @@ import ( "math/rand" "time" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -19,15 +20,16 @@ const ( ) type User struct { - ID uint `gorm:"primarykey"` - CreatedAt time.Time - UpdatedAt time.Time - Providers []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Username string `gorm:"not null;uniqueIndex"` - Role Role `gorm:"not null;default:user"` - GroupUserRelations []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"` + ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` + CreatedAt time.Time + UpdatedAt time.Time + Providers []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Username string `gorm:"not null;uniqueIndex"` + Role Role `gorm:"not null;default:user"` + GroupUserRelations []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"` } func (u *User) BeforeCreate(tx *gorm.DB) error { @@ -36,6 +38,9 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { if err == nil { u.Username = fmt.Sprintf("%s#%d", u.Username, rand.Intn(9999)) } + if u.ID == "" { + u.ID = uuid.NewString() + } return nil } diff --git a/internal/model/vendor.go b/internal/model/vendor.go new file mode 100644 index 0000000..ff7ce68 --- /dev/null +++ b/internal/model/vendor.go @@ -0,0 +1,24 @@ +package model + +import ( + "net/http" + "time" +) + +type StreamingVendor string + +const ( + StreamingVendorBilibili StreamingVendor = "bilibili" +) + +type StreamingVendorInfo struct { + UserID string `gorm:"not null;primarykey"` + Vendor StreamingVendor `gorm:"not null;primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + VendorToken +} + +type VendorToken struct { + Cookies []*http.Cookie `gorm:"serializer:fastjson"` +} diff --git a/internal/op/current.go b/internal/op/current.go index 154c9d0..03dd147 100644 --- a/internal/op/current.go +++ b/internal/op/current.go @@ -88,7 +88,7 @@ func (c *current) SetSeekRate(seek, rate, timeDiff float64) Status { func (c *Current) Proto() *pb.Current { return &pb.Current{ Movie: &pb.MovieInfo{ - Id: uint64(c.Movie.ID), + Id: c.Movie.ID, Base: &pb.BaseMovieInfo{ Url: c.Movie.Base.Url, Name: c.Movie.Base.Name, @@ -98,7 +98,6 @@ func (c *Current) Proto() *pb.Current { Type: c.Movie.Base.Type, Headers: c.Movie.Base.Headers, }, - PullKey: c.Movie.PullKey, CreatedAt: c.Movie.CreatedAt.UnixMilli(), Creator: GetUserName(c.Movie.CreatorID), }, diff --git a/internal/op/hub.go b/internal/op/hub.go index d7c61b5..30423a7 100644 --- a/internal/op/hub.go +++ b/internal/op/hub.go @@ -15,8 +15,8 @@ import ( ) type Hub struct { - id uint - clients rwmap.RWMap[uint, *Client] + id string + clients rwmap.RWMap[string, *Client] broadcast chan *broadcastMessage exit chan struct{} closed uint32 @@ -52,7 +52,7 @@ func WithIgnoreId(id ...string) BroadcastConf { } } -func newHub(id uint) *Hub { +func newHub(id string) *Hub { return &Hub{ id: id, broadcast: make(chan *broadcastMessage, 128), @@ -73,7 +73,7 @@ func (h *Hub) serve() error { select { case message := <-h.broadcast: h.devMessage(message.data) - h.clients.Range(func(_ uint, cli *Client) bool { + h.clients.Range(func(_ string, cli *Client) bool { if !message.sendToSelf { if cli.u.Username == message.sender { return true @@ -83,13 +83,13 @@ func (h *Hub) serve() error { return true } if err := cli.Send(message.data); err != nil { - log.Debugf("hub: %d, write to client err: %s\nmessage: %+v", h.id, err, message) + log.Debugf("hub: %s, write to client err: %s\nmessage: %+v", h.id, err, message) cli.Close() } return true }) case <-h.exit: - log.Debugf("hub: %d, closed", h.id) + log.Debugf("hub: %s, closed", h.id) return nil } } @@ -127,7 +127,7 @@ func (h *Hub) ping() { func (h *Hub) devMessage(msg Message) { switch msg.MessageType() { case websocket.TextMessage: - log.Debugf("hub: %d, broadcast:\nmessage: %+v", h.id, msg.String()) + log.Debugf("hub: %s, broadcast:\nmessage: %+v", h.id, msg.String()) } } @@ -144,7 +144,7 @@ func (h *Hub) Close() error { return ErrAlreadyClosed } close(h.exit) - h.clients.Range(func(_ uint, client *Client) bool { + h.clients.Range(func(_ string, client *Client) bool { h.clients.Delete(client.u.ID) client.Close() return true diff --git a/internal/op/movie.go b/internal/op/movie.go index 0ced439..8c0d45c 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -7,7 +7,6 @@ import ( "time" "github.com/go-resty/resty/v2" - "github.com/google/uuid" "github.com/synctv-org/synctv/internal/conf" "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/utils" @@ -38,9 +37,6 @@ func (m *movie) init() (err error) { if !conf.Conf.Rtmp.Enable { return errors.New("rtmp is not enabled") } - if m.PullKey == "" { - m.PullKey = uuid.NewString() - } if m.channel == nil { m.channel = rtmps.NewChannel() m.channel.InitHlsPlayer() @@ -58,7 +54,6 @@ func (m *movie) init() (err error) { } switch u.Scheme { case "rtmp": - m.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(m.Base.Url)).String() if m.channel == nil { m.channel = rtmps.NewChannel() m.channel.InitHlsPlayer() @@ -84,7 +79,6 @@ func (m *movie) init() (err error) { if m.Base.Type != "flv" { return errors.New("only flv is supported") } - m.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(m.Base.Url)).String() if m.channel == nil { m.channel = rtmps.NewChannel() m.channel.InitHlsPlayer() @@ -119,6 +113,9 @@ func (m *movie) init() (err error) { if !conf.Conf.Proxy.MovieProxy { return errors.New("movie proxy is not enabled") } + if m.Base.VendorInfo.Vendor != "" { + return errors.New("vendor movie info is not supported in movie proxy mode") + } u, err := url.Parse(m.Base.Url) if err != nil { return err @@ -129,16 +126,16 @@ func (m *movie) init() (err error) { if u.Scheme != "http" && u.Scheme != "https" { return errors.New("unsupported scheme") } - m.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(m.Base.Url)).String() case !m.Base.Live && !m.Base.Proxy, m.Base.Live && !m.Base.Proxy && !m.Base.RtmpSource: - u, err := url.Parse(m.Base.Url) - if err != nil { - return err - } - if u.Scheme != "http" && u.Scheme != "https" { - return errors.New("unsupported scheme") + if m.Base.VendorInfo.Vendor == "" { + u, err := url.Parse(m.Base.Url) + if err != nil { + return err + } + if u.Scheme != "http" && u.Scheme != "https" { + return errors.New("unsupported scheme") + } } - m.PullKey = "" default: return errors.New("unknown error") } @@ -158,7 +155,7 @@ func (m *movie) terminate() { } } -func (m *movie) Update(movie model.BaseMovieInfo) error { +func (m *movie) Update(movie model.BaseMovie) error { m.lock.Lock() defer m.lock.Unlock() m.terminate() diff --git a/internal/op/movies.go b/internal/op/movies.go index 9d8f5be..06fa26e 100644 --- a/internal/op/movies.go +++ b/internal/op/movies.go @@ -13,7 +13,7 @@ import ( ) type movies struct { - roomID uint + roomID string lock sync.RWMutex list dllist.Dllist[*movie] once sync.Once @@ -45,7 +45,6 @@ func (m *movies) Add(mo *model.Movie) error { Movie: mo, } - // You need to init to get the pullKey first, and then create it into the database. err := movie.init() if err != nil { return err @@ -61,22 +60,22 @@ func (m *movies) Add(mo *model.Movie) error { return nil } -func (m *movies) GetChannel(channelName string) (*rtmps.Channel, error) { - if channelName == "" { +func (m *movies) GetChannel(id string) (*rtmps.Channel, error) { + if id == "" { return nil, errors.New("channel name is nil") } m.lock.RLock() defer m.lock.RUnlock() m.init() for e := m.list.Front(); e != nil; e = e.Next() { - if e.Value.PullKey == channelName { + if e.Value.ID == id { return e.Value.Channel() } } return nil, errors.New("channel not found") } -func (m *movies) Update(movieId uint, movie model.BaseMovieInfo) error { +func (m *movies) Update(movieId string, movie model.BaseMovie) error { m.lock.Lock() defer m.lock.Unlock() m.init() @@ -116,7 +115,7 @@ func (m *movies) Close() error { return nil } -func (m *movies) DeleteMovieByID(id uint) error { +func (m *movies) DeleteMovieByID(id string) error { m.lock.Lock() defer m.lock.Unlock() m.init() @@ -135,13 +134,13 @@ func (m *movies) DeleteMovieByID(id uint) error { return errors.New("movie not found") } -func (m *movies) GetMovieByID(id uint) (*movie, error) { +func (m *movies) GetMovieByID(id string) (*movie, error) { m.lock.RLock() defer m.lock.RUnlock() return m.getMovieByID(id) } -func (m *movies) getMovieByID(id uint) (*movie, error) { +func (m *movies) getMovieByID(id string) (*movie, error) { m.init() for e := m.list.Front(); e != nil; e = e.Next() { if e.Value.ID == id { @@ -151,7 +150,7 @@ func (m *movies) getMovieByID(id uint) (*movie, error) { return nil, errors.New("movie not found") } -func (m *movies) getMovieElementByID(id uint) (*dllist.Element[*movie], error) { +func (m *movies) getMovieElementByID(id string) (*dllist.Element[*movie], error) { m.init() for e := m.list.Front(); e != nil; e = e.Next() { if e.Value.ID == id { @@ -161,20 +160,7 @@ func (m *movies) getMovieElementByID(id uint) (*dllist.Element[*movie], error) { return nil, errors.New("movie not found") } -func (m *movies) GetMovieWithPullKey(pullKey string) (*movie, error) { - m.lock.RLock() - defer m.lock.RUnlock() - m.init() - - for e := m.list.Front(); e != nil; e = e.Next() { - if e.Value.PullKey == pullKey { - return e.Value, nil - } - } - return nil, errors.New("movie not found") -} - -func (m *movies) SwapMoviePositions(id1, id2 uint) error { +func (m *movies) SwapMoviePositions(id1, id2 string) error { m.lock.Lock() defer m.lock.Unlock() m.init() diff --git a/internal/op/op.go b/internal/op/op.go index 7d07dec..d2739f5 100644 --- a/internal/op/op.go +++ b/internal/op/op.go @@ -9,7 +9,7 @@ import ( func Init(size int, ttl time.Duration) error { roomTTL = ttl - roomCache = synccache.NewSyncCache[uint, *Room](time.Minute*5, synccache.WithDeletedCallback[uint, *Room](func(v *Room) { + roomCache = synccache.NewSyncCache[string, *Room](time.Minute*5, synccache.WithDeletedCallback[string, *Room](func(v *Room) { v.close() })) userCache = gcache.New(size). diff --git a/internal/op/room.go b/internal/op/room.go index 633d4a5..09146f5 100644 --- a/internal/op/room.go +++ b/internal/op/room.go @@ -62,7 +62,7 @@ func (r *Room) CheckVersion(version uint32) bool { return atomic.LoadUint32(&r.version) == version } -func (r *Room) UpdateMovie(movieId uint, movie model.BaseMovieInfo) error { +func (r *Room) UpdateMovie(movieId string, movie model.BaseMovie) error { return r.movies.Update(movieId, movie) } @@ -71,7 +71,7 @@ func (r *Room) AddMovie(m model.Movie) error { return r.movies.Add(&m) } -func (r *Room) HasPermission(userID uint, permission model.Permission) bool { +func (r *Room) HasPermission(userID string, permission model.Permission) bool { ur, err := db.GetRoomUserRelation(r.ID, userID) if err != nil { return false @@ -100,23 +100,23 @@ func (r *Room) SetPassword(password string) error { return db.SetRoomHashedPassword(r.ID, hashedPassword) } -func (r *Room) SetUserRole(userID uint, role model.RoomRole) error { +func (r *Room) SetUserRole(userID string, role model.RoomRole) error { return db.SetUserRole(r.ID, userID, role) } -func (r *Room) SetUserPermission(userID uint, permission model.Permission) error { +func (r *Room) SetUserPermission(userID string, permission model.Permission) error { return db.SetUserPermission(r.ID, userID, permission) } -func (r *Room) AddUserPermission(userID uint, permission model.Permission) error { +func (r *Room) AddUserPermission(userID string, permission model.Permission) error { return db.AddUserPermission(r.ID, userID, permission) } -func (r *Room) RemoveUserPermission(userID uint, permission model.Permission) error { +func (r *Room) RemoveUserPermission(userID string, permission model.Permission) error { return db.RemoveUserPermission(r.ID, userID, permission) } -func (r *Room) DeleteUserPermission(userID uint) error { +func (r *Room) DeleteUserPermission(userID string) error { return db.DeleteUserPermission(r.ID, userID) } @@ -124,7 +124,7 @@ func (r *Room) GetMoviesCount() int { return r.movies.Len() } -func (r *Room) DeleteMovieByID(id uint) error { +func (r *Room) DeleteMovieByID(id string) error { return r.movies.DeleteMovieByID(id) } @@ -132,7 +132,7 @@ func (r *Room) ClearMovies() error { return r.movies.Clear() } -func (r *Room) GetMovieByID(id uint) (*movie, error) { +func (r *Room) GetMovieByID(id string) (*movie, error) { return r.movies.GetMovieByID(id) } @@ -141,7 +141,7 @@ func (r *Room) Current() *Current { return &c } -func (r *Room) ChangeCurrentMovie(id uint) error { +func (r *Room) ChangeCurrentMovie(id string) error { m, err := r.movies.GetMovieByID(id) if err != nil { return err @@ -150,14 +150,10 @@ func (r *Room) ChangeCurrentMovie(id uint) error { return nil } -func (r *Room) SwapMoviePositions(id1, id2 uint) error { +func (r *Room) SwapMoviePositions(id1, id2 string) error { return r.movies.SwapMoviePositions(id1, id2) } -func (r *Room) GetMovieWithPullKey(pullKey string) (*movie, error) { - return r.movies.GetMovieWithPullKey(pullKey) -} - func (r *Room) GetMoviesWithPage(page, pageSize int) []*movie { return r.movies.GetMoviesWithPage(page, pageSize) } diff --git a/internal/op/rooms.go b/internal/op/rooms.go index 6e39af3..14c65d7 100644 --- a/internal/op/rooms.go +++ b/internal/op/rooms.go @@ -12,7 +12,7 @@ import ( ) var roomTTL = time.Hour * 24 * 2 -var roomCache *synccache.SyncCache[uint, *Room] +var roomCache *synccache.SyncCache[string, *Room] func CreateRoom(name, password string, conf ...db.CreateRoomConfig) (*Room, error) { r, err := db.CreateRoom(name, password, conf...) @@ -53,7 +53,7 @@ func LoadOrInitRoom(room *model.Room) (*Room, bool) { return i.Value(), loaded } -func DeleteRoom(roomID uint) error { +func DeleteRoom(roomID string) error { err := db.DeleteRoomByID(roomID) if err != nil { return err @@ -61,7 +61,7 @@ func DeleteRoom(roomID uint) error { return CloseRoom(roomID) } -func CloseRoom(roomID uint) error { +func CloseRoom(roomID string) error { r, loaded := roomCache.LoadAndDelete(roomID) if loaded { r.Value().close() @@ -69,7 +69,7 @@ func CloseRoom(roomID uint) error { return errors.New("room not found in cache") } -func LoadRoomByID(id uint) (*Room, error) { +func LoadRoomByID(id string) (*Room, error) { r2, loaded := roomCache.Load(id) if loaded { r2.SetExpiration(time.Now().Add(roomTTL)) @@ -78,7 +78,10 @@ func LoadRoomByID(id uint) (*Room, error) { return nil, errors.New("room not found") } -func LoadOrInitRoomByID(id uint) (*Room, error) { +func LoadOrInitRoomByID(id string) (*Room, error) { + if len(id) != 36 { + return nil, errors.New("room id is not 32 bit") + } i, loaded := roomCache.Load(id) if loaded { i.SetExpiration(time.Now().Add(roomTTL)) @@ -92,7 +95,7 @@ func LoadOrInitRoomByID(id uint) (*Room, error) { return r, nil } -func ClientNum(roomID uint) int64 { +func ClientNum(roomID string) int64 { r, loaded := roomCache.Load(roomID) if loaded { return r.Value().ClientNum() @@ -100,7 +103,7 @@ func ClientNum(roomID uint) int64 { return 0 } -func HasRoom(roomID uint) bool { +func HasRoom(roomID string) bool { _, ok := roomCache.Load(roomID) if ok { return true @@ -120,7 +123,7 @@ func HasRoomByName(name string) bool { return ok } -func SetRoomPassword(roomID uint, password string) error { +func SetRoomPassword(roomID, password string) error { r, err := LoadOrInitRoomByID(roomID) if err != nil { return err @@ -130,7 +133,7 @@ func SetRoomPassword(roomID uint, password string) error { func GetAllRoomsInCacheWithNoNeedPassword() []*Room { rooms := make([]*Room, 0) - roomCache.Range(func(key uint, value *synccache.Entry[*Room]) bool { + roomCache.Range(func(key string, value *synccache.Entry[*Room]) bool { v := value.Value() if !v.NeedPassword() { rooms = append(rooms, v) @@ -142,7 +145,7 @@ func GetAllRoomsInCacheWithNoNeedPassword() []*Room { func GetAllRoomsInCacheWithoutHidden() []*Room { rooms := make([]*Room, 0) - roomCache.Range(func(key uint, value *synccache.Entry[*Room]) bool { + roomCache.Range(func(key string, value *synccache.Entry[*Room]) bool { v := value.Value() if !v.Settings.Hidden { rooms = append(rooms, v) @@ -153,11 +156,11 @@ func GetAllRoomsInCacheWithoutHidden() []*Room { } type RoomHeapItem struct { - ID uint + ID string RoomName string ClientNum int64 NeedPassword bool - CreatorID uint + CreatorID string CreatedAt time.Time } @@ -189,7 +192,7 @@ func (h *RoomHeap) Pop() *RoomHeapItem { func GetRoomHeapInCacheWithoutHidden() RoomHeap { rooms := make(RoomHeap, 0) - roomCache.Range(func(key uint, value *synccache.Entry[*Room]) bool { + roomCache.Range(func(key string, value *synccache.Entry[*Room]) bool { v := value.Value() if !v.Settings.Hidden { heap.Push[*RoomHeapItem](&rooms, &RoomHeapItem{ diff --git a/internal/op/user.go b/internal/op/user.go index 235a58a..b6bd51a 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -15,9 +15,9 @@ func (u *User) CreateRoom(name, password string, conf ...db.CreateRoomConfig) (* return db.CreateRoom(name, password, append(conf, db.WithCreator(&u.User))...) } -func (u *User) NewMovie(movie model.MovieInfo) model.Movie { +func (u *User) NewMovie(movie model.BaseMovie) model.Movie { return model.Movie{ - MovieInfo: movie, + Base: movie, CreatorID: u.ID, } } @@ -38,7 +38,7 @@ func (u *User) IsPending() bool { return u.Role == model.RolePending } -func (u *User) HasPermission(roomID uint, permission model.Permission) bool { +func (u *User) HasPermission(roomID string, permission model.Permission) bool { if u.Role >= model.RoleAdmin { return true } @@ -49,14 +49,14 @@ func (u *User) HasPermission(roomID uint, permission model.Permission) bool { return ur.HasPermission(permission) } -func (u *User) DeleteRoom(roomID uint) error { +func (u *User) DeleteRoom(roomID string) error { if !u.HasPermission(roomID, model.CanDeleteRoom) { return errors.New("no permission") } return DeleteRoom(roomID) } -func (u *User) SetRoomPassword(roomID uint, password string) error { +func (u *User) SetRoomPassword(roomID, password string) error { if !u.HasPermission(roomID, model.CanSetRoomPassword) { return errors.New("no permission") } diff --git a/internal/op/users.go b/internal/op/users.go index 47e2d98..53a222a 100644 --- a/internal/op/users.go +++ b/internal/op/users.go @@ -13,7 +13,7 @@ import ( var userCache gcache.Cache -func GetUserById(id uint) (*User, error) { +func GetUserById(id string) (*User, error) { i, err := userCache.Get(id) if err == nil { return i.(*User), nil @@ -72,14 +72,14 @@ func GetUserByProvider(p provider.OAuth2Provider, pid uint) (*User, error) { return GetUserById(uid) } -func DeleteUserByID(userID uint) error { +func DeleteUserByID(userID string) error { err := db.DeleteUserByID(userID) if err != nil { return err } userCache.Remove(userID) - roomCache.Range(func(key uint, value *synccache.Entry[*Room]) bool { + roomCache.Range(func(key string, value *synccache.Entry[*Room]) bool { v := value.Value() if v.CreatorID == userID { roomCache.CompareAndDelete(key, value) @@ -95,7 +95,7 @@ func SaveUser(u *model.User) error { return db.SaveUser(u) } -func GetUserName(userID uint) string { +func GetUserName(userID string) string { u, err := GetUserById(userID) if err != nil { return "" @@ -103,7 +103,7 @@ func GetUserName(userID uint) string { return u.Username } -func SetRoleByID(userID uint, role model.Role) error { +func SetRoleByID(userID string, role model.Role) error { err := db.SetRoleByID(userID, role) if err != nil { return err diff --git a/internal/rtmp/rtmp.go b/internal/rtmp/rtmp.go index a7bf999..843dd22 100644 --- a/internal/rtmp/rtmp.go +++ b/internal/rtmp/rtmp.go @@ -14,11 +14,11 @@ import ( var s *rtmps.Server type RtmpClaims struct { - PullKey string `json:"p"` + MovieID string `json:"m"` jwt.RegisteredClaims } -func AuthRtmpPublish(Authorization string) (channelName string, err error) { +func AuthRtmpPublish(Authorization string) (movieID string, err error) { t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &RtmpClaims{}, func(token *jwt.Token) (any, error) { return stream.StringToBytes(conf.Conf.Jwt.Secret), nil }) @@ -29,12 +29,12 @@ func AuthRtmpPublish(Authorization string) (channelName string, err error) { if !ok { return "", errors.New("auth failed") } - return claims.PullKey, nil + return claims.MovieID, nil } -func NewRtmpAuthorization(channelName string) (string, error) { +func NewRtmpAuthorization(movieID string) (string, error) { claims := &RtmpClaims{ - PullKey: channelName, + MovieID: movieID, RegisteredClaims: jwt.RegisteredClaims{ NotBefore: jwt.NewNumericDate(time.Now()), }, diff --git a/proto/message/message.pb.go b/proto/message/message.pb.go index 69979a1..ed9285e 100644 --- a/proto/message/message.pb.go +++ b/proto/message/message.pb.go @@ -199,11 +199,10 @@ type MovieInfo struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Base *BaseMovieInfo `protobuf:"bytes,2,opt,name=base,proto3" json:"base,omitempty"` - PullKey string `protobuf:"bytes,3,opt,name=pullKey,proto3" json:"pullKey,omitempty"` - CreatedAt int64 `protobuf:"varint,4,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - Creator string `protobuf:"bytes,5,opt,name=creator,proto3" json:"creator,omitempty"` + CreatedAt int64 `protobuf:"varint,3,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + Creator string `protobuf:"bytes,4,opt,name=creator,proto3" json:"creator,omitempty"` } func (x *MovieInfo) Reset() { @@ -238,11 +237,11 @@ func (*MovieInfo) Descriptor() ([]byte, []int) { return file_proto_message_message_proto_rawDescGZIP(), []int{1} } -func (x *MovieInfo) GetId() uint64 { +func (x *MovieInfo) GetId() string { if x != nil { return x.Id } - return 0 + return "" } func (x *MovieInfo) GetBase() *BaseMovieInfo { @@ -252,13 +251,6 @@ func (x *MovieInfo) GetBase() *BaseMovieInfo { return nil } -func (x *MovieInfo) GetPullKey() string { - if x != nil { - return x.PullKey - } - return "" -} - func (x *MovieInfo) GetCreatedAt() int64 { if x != nil { return x.CreatedAt @@ -516,58 +508,56 @@ var file_proto_message_message_proto_rawDesc = []byte{ 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x97, 0x01, 0x0a, 0x09, 0x4d, 0x6f, 0x76, 0x69, 0x65, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x28, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x6f, 0x76, 0x69, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, - 0x75, 0x6c, 0x6c, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x75, - 0x6c, 0x6c, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x4a, 0x0a, - 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x65, 0x65, 0x6b, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x73, 0x65, 0x65, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x72, - 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x69, 0x6e, 0x67, 0x22, 0x58, 0x0a, 0x07, 0x43, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6d, 0x6f, 0x76, 0x69, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x6f, 0x76, 0x69, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x6d, 0x6f, 0x76, 0x69, 0x65, 0x12, 0x25, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x86, 0x02, 0x0a, 0x0e, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, - 0x65, 0x65, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x73, 0x65, 0x65, 0x6b, 0x12, - 0x2d, 0x0a, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, - 0x48, 0x00, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1c, - 0x0a, 0x09, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x09, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x4e, 0x75, 0x6d, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, - 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x2a, 0xdb, 0x01, 0x0a, - 0x12, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x43, - 0x48, 0x41, 0x54, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x02, 0x12, 0x08, 0x0a, - 0x04, 0x50, 0x4c, 0x41, 0x59, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x55, 0x53, 0x45, - 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x53, 0x45, 0x45, 0x4b, - 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x4f, 0x4f, 0x5f, 0x46, 0x41, 0x53, 0x54, 0x10, 0x06, - 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x4f, 0x4f, 0x5f, 0x53, 0x4c, 0x4f, 0x57, 0x10, 0x07, 0x12, 0x0f, - 0x0a, 0x0b, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x10, 0x08, 0x12, - 0x0f, 0x0a, 0x0b, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x53, 0x45, 0x45, 0x4b, 0x10, 0x09, - 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x43, 0x55, 0x52, 0x52, 0x45, - 0x4e, 0x54, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, - 0x4f, 0x56, 0x49, 0x45, 0x53, 0x10, 0x0b, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, 0x4e, 0x47, - 0x45, 0x5f, 0x50, 0x45, 0x4f, 0x50, 0x4c, 0x45, 0x10, 0x0c, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x3b, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x02, 0x38, 0x01, 0x22, 0x7d, 0x0a, 0x09, 0x4d, 0x6f, 0x76, 0x69, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x28, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x6f, 0x76, 0x69, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x6f, 0x72, 0x22, 0x4a, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x73, 0x65, 0x65, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x73, 0x65, 0x65, 0x6b, + 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, + 0x72, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x69, 0x6e, 0x67, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x69, 0x6e, 0x67, 0x22, 0x58, + 0x0a, 0x07, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6d, 0x6f, 0x76, + 0x69, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x4d, 0x6f, 0x76, 0x69, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x6d, 0x6f, 0x76, 0x69, + 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x86, 0x02, 0x0a, 0x0e, 0x45, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x72, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x65, 0x65, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x04, + 0x73, 0x65, 0x65, 0x6b, 0x12, 0x2d, 0x0a, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x4e, 0x75, 0x6d, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x4e, 0x75, + 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x74, 0x69, 0x6d, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x2a, 0xdb, 0x01, 0x0a, 0x12, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, + 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x48, 0x41, 0x54, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, + 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4c, 0x41, 0x59, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, + 0x50, 0x41, 0x55, 0x53, 0x45, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x48, 0x45, 0x43, 0x4b, + 0x5f, 0x53, 0x45, 0x45, 0x4b, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x4f, 0x4f, 0x5f, 0x46, + 0x41, 0x53, 0x54, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x4f, 0x4f, 0x5f, 0x53, 0x4c, 0x4f, + 0x57, 0x10, 0x07, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x52, 0x41, + 0x54, 0x45, 0x10, 0x08, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x53, + 0x45, 0x45, 0x4b, 0x10, 0x09, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, + 0x43, 0x55, 0x52, 0x52, 0x45, 0x4e, 0x54, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, + 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x56, 0x49, 0x45, 0x53, 0x10, 0x0b, 0x12, 0x11, 0x0a, 0x0d, + 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x50, 0x45, 0x4f, 0x50, 0x4c, 0x45, 0x10, 0x0c, 0x42, + 0x06, 0x5a, 0x04, 0x2e, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/message/message.proto b/proto/message/message.proto index 8e91b88..bd13f52 100644 --- a/proto/message/message.proto +++ b/proto/message/message.proto @@ -30,11 +30,10 @@ message BaseMovieInfo { } message MovieInfo { - uint64 id = 1; + string id = 1; BaseMovieInfo base = 2; - string pullKey = 3; - int64 createdAt = 4; - string creator = 5; + int64 createdAt = 3; + string creator = 4; } message Status { diff --git a/proxy/read_seeker.go b/proxy/read_seeker.go index 46b4077..3bcf422 100644 --- a/proxy/read_seeker.go +++ b/proxy/read_seeker.go @@ -31,6 +31,9 @@ func WithHeaders(headers map[string]string) HttpReadSeekerConf { func WithAppendHeaders(headers map[string]string) HttpReadSeekerConf { return func(h *HttpReadSeeker) { + if h.headers == nil { + h.headers = make(map[string]string) + } for k, v := range headers { h.headers[k] = v } diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 9bf34aa..fbf0a35 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -177,7 +177,7 @@ func PendingUsers(ctx *gin.Context) { })) } -func ApprovePendingUser(Authorization string, userID uint) error { +func ApprovePendingUser(Authorization, userID string) error { user, err := op.GetUserById(userID) if err != nil { return err diff --git a/server/handlers/init.go b/server/handlers/init.go index 68f15a4..53cad46 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "github.com/synctv-org/synctv/public" + Vbilibili "github.com/synctv-org/synctv/server/handlers/vendors/bilibili" "github.com/synctv-org/synctv/server/middlewares" "github.com/synctv-org/synctv/utils" ) @@ -106,16 +107,16 @@ func Init(e *gin.Engine) { needAuthMovie.POST("/clear", ClearMovies) - movie.HEAD("/proxy/:roomId/:pullKey", ProxyMovie) + movie.HEAD("/proxy/:roomId/:movieId", ProxyMovie) - movie.GET("/proxy/:roomId/:pullKey", ProxyMovie) + movie.GET("/proxy/:roomId/:movieId", ProxyMovie) { live := needAuthMovie.Group("/live") live.POST("/publishKey", NewPublishKey) - live.GET("/*pullKey", JoinLive) + live.GET("/*movieId", JoinLive) } } @@ -131,5 +132,19 @@ func Init(e *gin.Engine) { needAuthUser.POST("/username", SetUsername) } + + { + vendor := needAuthUserApi.Group("/vendor") + + { + bilibili := vendor.Group("/bilibili") + + bilibili.GET("/qr", Vbilibili.QRCode) + + bilibili.POST("/login", Vbilibili.Login) + + bilibili.POST("/parse", Vbilibili.Parse) + } + } } } diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 2ff7a4c..997ffc6 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-resty/resty/v2" "github.com/synctv-org/synctv/internal/conf" + "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/rtmp" @@ -20,6 +21,7 @@ import ( "github.com/synctv-org/synctv/proxy" "github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/utils" + "github.com/synctv-org/synctv/vendors/bilibili" "github.com/zijiren233/livelib/protocol/hls" "github.com/zijiren233/livelib/protocol/httpflv" ) @@ -62,34 +64,54 @@ func MovieList(ctx *gin.Context) { mresp[i] = model.MoviesResp{ Id: v.ID, Base: m[i].Base, - PullKey: v.PullKey, - Creater: op.GetUserName(v.CreatorID), + Creator: op.GetUserName(v.CreatorID), } } - current := room.Current() + current, err := genCurrent(room) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "current": &model.CurrentMovieResp{ - Status: current.Status, - Movie: model.MoviesResp{ - Id: current.Movie.ID, - Base: current.Movie.Base, - PullKey: current.Movie.PullKey, - Creater: op.GetUserName(current.Movie.CreatorID), - }, - }, - "total": room.GetMoviesCount(), - "movies": mresp, + "current": genCurrentResp(current), + "total": room.GetMoviesCount(), + "movies": mresp, })) } +func genCurrent(room *op.Room) (*op.Current, error) { + current := room.Current() + if current.Movie.Base.Vendor != "" { + return current, parse2VendorMovie(¤t.Movie) + } + return current, nil +} + +func genCurrentResp(current *op.Current) *model.CurrentMovieResp { + return &model.CurrentMovieResp{ + Status: current.Status, + Movie: model.MoviesResp{ + Id: current.Movie.ID, + Base: current.Movie.Base, + Creator: op.GetUserName(current.Movie.CreatorID), + }, + } +} + func CurrentMovie(ctx *gin.Context) { room := ctx.MustGet("room").(*op.Room) // user := ctx.MustGet("user").(*op.User) + current, err := genCurrent(room) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "current": room.Current(), + "current": genCurrentResp(current), })) } @@ -110,8 +132,7 @@ func Movies(ctx *gin.Context) { mresp[i] = model.MoviesResp{ Id: v.ID, Base: m[i].Base, - PullKey: v.PullKey, - Creater: op.GetUserName(v.CreatorID), + Creator: op.GetUserName(v.CreatorID), } } @@ -131,9 +152,7 @@ func PushMovie(ctx *gin.Context) { return } - mi := user.NewMovie(dbModel.MovieInfo{ - Base: dbModel.BaseMovieInfo(req), - }) + mi := user.NewMovie(dbModel.BaseMovie(req)) err := room.AddMovie(mi) if err != nil { @@ -179,12 +198,7 @@ func NewPublishKey(ctx *gin.Context) { return } - if movie.PullKey == "" { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("pull key is empty")) - return - } - - token, err := rtmp.NewRtmpAuthorization(movie.PullKey) + token, err := rtmp.NewRtmpAuthorization(movie.ID) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return @@ -212,7 +226,7 @@ func EditMovie(ctx *gin.Context) { return } - if err := room.UpdateMovie(req.Id, dbModel.BaseMovieInfo(req.PushMovieReq)); err != nil { + if err := room.UpdateMovie(req.Id, dbModel.BaseMovie(req.PushMovieReq)); err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -345,29 +359,27 @@ var allowedProxyMovieContentType = map[string]struct{}{ "video/webm": {}, } -const UserAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.40` - func ProxyMovie(ctx *gin.Context) { roomId := ctx.Param("roomId") if roomId == "" { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("roomId is empty")) return } - id, err := strconv.ParseUint(roomId, 10, 64) + + room, err := op.LoadOrInitRoomByID(roomId) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } - room, err := op.LoadOrInitRoomByID(uint(id)) + m, err := room.GetMovieByID(ctx.Param("movieId")) if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } - m, err := room.GetMovieWithPullKey(ctx.Param("pullKey")) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + if m.Base.VendorInfo.Vendor != "" { + ProxyVendorMovie(ctx, m.Movie) return } @@ -412,7 +424,6 @@ func ProxyMovie(ctx *gin.Context) { hrs := proxy.NewBufferedHttpReadSeeker(128*1024, m.Base.Url, proxy.WithContext(ctx), proxy.WithHeaders(m.Base.Headers), - proxy.WithContext(ctx), proxy.WithContentLength(length), ) name := resp.Header().Get("Content-Disposition") @@ -438,16 +449,11 @@ func JoinLive(ctx *gin.Context) { room := ctx.MustGet("room").(*op.Room) // user := ctx.MustGet("user").(*op.User) - pullKey := strings.Trim(ctx.Param("pullKey"), "/") - pullKeySplitd := strings.Split(pullKey, "/") - fileName := pullKeySplitd[0] - fileExt := path.Ext(pullKey) + movieId := strings.Trim(ctx.Param("movieId"), "/") + movieIdSplitd := strings.Split(movieId, "/") + fileName := movieIdSplitd[0] + fileExt := path.Ext(movieId) channelName := strings.TrimSuffix(fileName, fileExt) - // m, err := room.GetMovieWithPullKey(channelName) - // if err != nil { - // ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) - // return - // } channel, err := room.GetChannel(channelName) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) @@ -469,7 +475,7 @@ func JoinLive(ctx *gin.Context) { } ctx.Data(http.StatusOK, hls.M3U8ContentType, b.Bytes()) case ".ts": - b, err := channel.GetTsFile(pullKeySplitd[1]) + b, err := channel.GetTsFile(movieIdSplitd[1]) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return @@ -481,3 +487,159 @@ func JoinLive(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt))) } } + +func ProxyVendorMovie(ctx *gin.Context, m *dbModel.Movie) { + if m.Base.VendorInfo.Vendor == "" { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor is empty")) + return + } + + switch m.Base.VendorInfo.Vendor { + case dbModel.StreamingVendorBilibili: + bvidI := m.Base.VendorInfo.Info["bvid"] + epIdI := m.Base.VendorInfo.Info["epId"] + if bvidI != nil && epIdI != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("bvid(%v) and epId(%v) can't be used at the same time", bvidI, epIdI))) + return + } + + var ( + bvid string + epId float64 + cid float64 + ok bool + ) + if bvidI != nil { + bvid, ok = bvidI.(string) + if !ok { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("bvid is not string")) + return + } + } else if epIdI != nil { + epId, ok = epIdI.(float64) + if !ok { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("epId is not number")) + return + } + } else { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("bvid and epId is empty")) + return + } + + cidI := m.Base.VendorInfo.Info["cid"] + if cidI == nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cid is empty")) + return + } + cid, ok = cidI.(float64) + if !ok { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cid is not number")) + return + } + + vendor, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(m.CreatorID, dbModel.StreamingVendorBilibili) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + cli := bilibili.NewClient(vendor.Cookies) + + if bvid != "" { + mu, err := cli.GetVideoURL(0, bvid, uint(cid)) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + // s, err := cli.GetSubtitles(0, bvid, uint(cid)) + // if err != nil { + // ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + // return + // } + ctx.Redirect(http.StatusFound, mu.URL) + return + } else { + mu, err := cli.GetPGCURL(uint(epId), uint(cid)) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + hrs := proxy.NewBufferedHttpReadSeeker(128*1024, mu.URL, + proxy.WithContext(ctx), + proxy.WithAppendHeaders(map[string]string{ + "Referer": "https://www.bilibili.com/", + "User-Agent": utils.UA, + }), + ) + http.ServeContent(ctx.Writer, ctx.Request, mu.URL, time.Now(), hrs) + return + } + + default: + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support")) + return + } +} + +func parse2VendorMovie(movie *dbModel.Movie) error { + switch movie.Base.VendorInfo.Vendor { + case dbModel.StreamingVendorBilibili: + bvidI := movie.Base.VendorInfo.Info["bvid"] + epIdI := movie.Base.VendorInfo.Info["epId"] + if bvidI != nil && epIdI != nil { + return fmt.Errorf("bvid(%v) and epId(%v) can't be used at the same time", bvidI, epIdI) + } + + var ( + bvid string + // epId float64 + cid float64 + ok bool + ) + if bvidI != nil { + bvid, ok = bvidI.(string) + if !ok { + return fmt.Errorf("bvid is not string") + } + } else if epIdI != nil { + // epId, ok = epIdI.(float64) + // if !ok { + // return fmt.Errorf("epId is not number") + // } + } else { + return fmt.Errorf("bvid and epId is empty") + } + + cidI := movie.Base.VendorInfo.Info["cid"] + if cidI == nil { + return fmt.Errorf("cid is empty") + } + cid, ok = cidI.(float64) + if !ok { + return fmt.Errorf("cid is not number") + } + + vendor, err := db.AssignFirstOrCreateVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili) + if err != nil { + return err + } + cli := bilibili.NewClient(vendor.Cookies) + + if bvid != "" { + mu, err := cli.GetVideoURL(0, bvid, uint(cid)) + if err != nil { + return err + } + movie.Base.Url = mu.URL + return nil + } else { + // mu, err := cli.GetPGCURL(uint(epId), uint(cid)) + // if err != nil { + // return err + // } + return nil + } + + default: + return fmt.Errorf("vendor not support") + } +} diff --git a/server/handlers/room.go b/server/handlers/room.go index 336b409..6b8edb7 100644 --- a/server/handlers/room.go +++ b/server/handlers/room.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/synctv-org/synctv/internal/db" @@ -190,13 +189,7 @@ func genRoomListResp(scopes ...func(db *gorm.DB) *gorm.DB) []*model.RoomListResp } func CheckRoom(ctx *gin.Context) { - id, err := strconv.Atoi(ctx.Query("roomId")) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - - r, err := db.GetRoomByID(uint(id)) + r, err := db.GetRoomByID(ctx.Query("roomId")) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return diff --git a/server/handlers/vendors/bilibili/bilibili.go b/server/handlers/vendors/bilibili/bilibili.go new file mode 100644 index 0000000..5725325 --- /dev/null +++ b/server/handlers/vendors/bilibili/bilibili.go @@ -0,0 +1,150 @@ +package Vbilibili + +import ( + "errors" + "net/http" + "strconv" + + "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/server/model" + "github.com/synctv-org/synctv/vendors/bilibili" +) + +func QRCode(ctx *gin.Context) { + r, err := bilibili.NewQRCode() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(r)) +} + +type LoginReq struct { + Key string `json:"key"` +} + +func (r *LoginReq) Validate() error { + if r.Key == "" { + return errors.New("key is empty") + } + return nil +} + +func (r *LoginReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func Login(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := LoginReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + cookie, err := bilibili.Login(req.Key) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + _, err = db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie([]*http.Cookie{cookie})) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +type ParseReq struct { + URL string `json:"url"` +} + +func (r *ParseReq) Validate() error { + if r.URL == "" { + return errors.New("url is empty") + } + return nil +} + +func (r *ParseReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func Parse(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := ParseReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + matchType, id, err := bilibili.Match(req.URL) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + vendor, err := db.FirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + cli := bilibili.NewClient(vendor.Cookies) + + switch matchType { + case "bv": + mpis, err := cli.ParseVideoPage(0, id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(mpis)) + case "av": + aid, err := strconv.Atoi(id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + mpis, err := cli.ParseVideoPage(uint(aid), "") + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(mpis)) + case "ep": + epId, err := strconv.Atoi(id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + mpis, err := cli.ParsePGCPage(uint(epId), 0) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(mpis)) + case "ss": + seasonId, err := strconv.Atoi(id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + mpis, err := cli.ParsePGCPage(0, uint(seasonId)) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(mpis)) + default: + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("unknown match type")) + return + } +} diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 9a68898..aa6106e 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -20,13 +20,13 @@ var ( ) type AuthClaims struct { - UserId uint `json:"u"` + UserId string `json:"u"` jwt.RegisteredClaims } type AuthRoomClaims struct { AuthClaims - RoomId uint `json:"r"` + RoomId string `json:"r"` Version uint32 `json:"rv"` } @@ -64,11 +64,11 @@ func AuthRoom(Authorization string) (*op.User, *op.Room, error) { return nil, nil, err } - if claims.RoomId == 0 { + if len(claims.RoomId) != 36 { return nil, nil, ErrAuthFailed } - if claims.UserId == 0 { + if len(claims.UserId) != 36 { return nil, nil, ErrAuthFailed } @@ -100,7 +100,7 @@ func AuthUser(Authorization string) (*op.User, error) { return nil, err } - if claims.UserId == 0 { + if len(claims.UserId) != 36 { return nil, ErrAuthFailed } diff --git a/server/model/movie.go b/server/model/movie.go index 1fd62d8..7551927 100644 --- a/server/model/movie.go +++ b/server/model/movie.go @@ -20,7 +20,7 @@ var ( ErrEmptyIds = errors.New("empty ids") ) -type PushMovieReq model.BaseMovieInfo +type PushMovieReq model.BaseMovie func (p *PushMovieReq) Decode(ctx *gin.Context) error { return json.NewDecoder(ctx.Request.Body).Decode(p) @@ -41,11 +41,15 @@ func (p *PushMovieReq) Validate() error { return ErrTypeTooLong } + if p.VendorInfo.Vendor != "" && p.VendorInfo.Info == nil { + return errors.New("vendor info is empty") + } + return nil } type IdReq struct { - Id uint `json:"id"` + Id string `json:"id"` } func (i *IdReq) Decode(ctx *gin.Context) error { @@ -53,7 +57,7 @@ func (i *IdReq) Decode(ctx *gin.Context) error { } func (i *IdReq) Validate() error { - if i.Id <= 0 { + if len(i.Id) != 36 { return ErrId } return nil @@ -79,7 +83,7 @@ func (e *EditMovieReq) Validate() error { } type IdsReq struct { - Ids []uint `json:"ids"` + Ids []string `json:"ids"` } func (i *IdsReq) Decode(ctx *gin.Context) error { @@ -90,12 +94,17 @@ func (i *IdsReq) Validate() error { if len(i.Ids) == 0 { return ErrEmptyIds } + for _, v := range i.Ids { + if len(v) != 36 { + return ErrId + } + } return nil } type SwapMovieReq struct { - Id1 uint `json:"id1"` - Id2 uint `json:"id2"` + Id1 string `json:"id1"` + Id2 string `json:"id2"` } func (s *SwapMovieReq) Decode(ctx *gin.Context) error { @@ -103,17 +112,16 @@ func (s *SwapMovieReq) Decode(ctx *gin.Context) error { } func (s *SwapMovieReq) Validate() error { - if s.Id1 <= 0 || s.Id2 <= 0 { + if len(s.Id1) != 36 || len(s.Id2) != 36 { return ErrId } return nil } type MoviesResp struct { - Id uint `json:"id"` - Base model.BaseMovieInfo `json:"base"` - PullKey string `json:"pullKey"` - Creater string `json:"creater"` + Id string `json:"id"` + Base model.BaseMovie `json:"base"` + Creator string `json:"creator"` } type CurrentMovieResp struct { diff --git a/server/model/room.go b/server/model/room.go index 2cd28b7..2e494dc 100644 --- a/server/model/room.go +++ b/server/model/room.go @@ -71,7 +71,7 @@ func (c *CreateRoomReq) Validate() error { } type RoomListResp struct { - RoomId uint `json:"roomId"` + RoomId string `json:"roomId"` RoomName string `json:"roomName"` PeopleNum int64 `json:"peopleNum"` NeedPassword bool `json:"needPassword"` @@ -80,7 +80,7 @@ type RoomListResp struct { } type LoginRoomReq struct { - RoomId uint `json:"roomId"` + RoomId string `json:"roomId"` Password string `json:"password"` } @@ -89,7 +89,7 @@ func (l *LoginRoomReq) Decode(ctx *gin.Context) error { } func (l *LoginRoomReq) Validate() error { - if l.RoomId == 0 { + if len(l.RoomId) != 36 { return ErrEmptyRoomName } @@ -114,7 +114,7 @@ func (s *SetRoomPasswordReq) Validate() error { } type UserIdReq struct { - UserId uint `json:"userId"` + UserId string `json:"userId"` } func (u *UserIdReq) Decode(ctx *gin.Context) error { @@ -122,7 +122,7 @@ func (u *UserIdReq) Decode(ctx *gin.Context) error { } func (u *UserIdReq) Validate() error { - if u.UserId == 0 { + if len(u.UserId) != 36 { return ErrEmptyUserId } return nil diff --git a/server/model/user.go b/server/model/user.go index ddac9cb..6cb5e67 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -52,7 +52,7 @@ func (l *LoginUserReq) Validate() error { } type UserInfoResp struct { - ID uint `json:"id"` + ID string `json:"id"` Username string `json:"username"` Role dbModel.Role `json:"role"` CreatedAt int64 `json:"createdAt"` @@ -78,7 +78,7 @@ func (s *SetUsernameReq) Decode(ctx *gin.Context) error { } type UserIDReq struct { - ID uint `json:"id"` + ID string `json:"id"` } func (u *UserIDReq) Decode(ctx *gin.Context) error { @@ -86,7 +86,7 @@ func (u *UserIDReq) Decode(ctx *gin.Context) error { } func (u *UserIDReq) Validate() error { - if u.ID == 0 { + if len(u.ID) != 36 { return errors.New("id is required") } return nil diff --git a/utils/utils.go b/utils/utils.go index 0ec8f2d..25a1a7a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -19,6 +19,10 @@ import ( var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +const ( + UA = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.69` +) + func RandString(n int) string { b := make([]rune, n) for i := range b { diff --git a/vendors/bilibili/bilibili.go b/vendors/bilibili/bilibili.go new file mode 100644 index 0000000..444e9c1 --- /dev/null +++ b/vendors/bilibili/bilibili.go @@ -0,0 +1,814 @@ +package bilibili + +type qrcodeResp struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + URL string `json:"url"` + QrcodeKey string `json:"qrcode_key"` + } `json:"data"` +} + +type videoPageInfo struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + Bvid string `json:"bvid"` + Aid uint `json:"aid"` + Videos int `json:"videos"` + Tid int `json:"tid"` + Tname string `json:"tname"` + Copyright int `json:"copyright"` + Pic string `json:"pic"` + Title string `json:"title"` + Pubdate int `json:"pubdate"` + Ctime int `json:"ctime"` + Desc string `json:"desc"` + DescV2 []struct { + RawText string `json:"raw_text"` + Type int `json:"type"` + BizID int `json:"biz_id"` + } `json:"desc_v2"` + State int `json:"state"` + Duration int `json:"duration"` + Rights struct { + Bp int `json:"bp"` + Elec int `json:"elec"` + Download int `json:"download"` + Movie int `json:"movie"` + Pay int `json:"pay"` + Hd5 int `json:"hd5"` + NoReprint int `json:"no_reprint"` + Autoplay int `json:"autoplay"` + UgcPay int `json:"ugc_pay"` + IsCooperation int `json:"is_cooperation"` + UgcPayPreview int `json:"ugc_pay_preview"` + NoBackground int `json:"no_background"` + CleanMode int `json:"clean_mode"` + IsSteinGate int `json:"is_stein_gate"` + Is360 int `json:"is_360"` + NoShare int `json:"no_share"` + ArcPay int `json:"arc_pay"` + FreeWatch int `json:"free_watch"` + } `json:"rights"` + Owner struct { + Mid int `json:"mid"` + Name string `json:"name"` + Face string `json:"face"` + } `json:"owner"` + Stat struct { + Aid int `json:"aid"` + View int `json:"view"` + Danmaku int `json:"danmaku"` + Reply int `json:"reply"` + Favorite int `json:"favorite"` + Coin int `json:"coin"` + Share int `json:"share"` + NowRank int `json:"now_rank"` + HisRank int `json:"his_rank"` + Like int `json:"like"` + Dislike int `json:"dislike"` + Evaluation string `json:"evaluation"` + ArgueMsg string `json:"argue_msg"` + Vt int `json:"vt"` + } `json:"stat"` + Dynamic string `json:"dynamic"` + Cid int `json:"cid"` + Dimension struct { + Width int `json:"width"` + Height int `json:"height"` + Rotate int `json:"rotate"` + } `json:"dimension"` + SeasonID int `json:"season_id"` + Premiere interface{} `json:"premiere"` + TeenageMode int `json:"teenage_mode"` + IsChargeableSeason bool `json:"is_chargeable_season"` + IsStory bool `json:"is_story"` + IsUpowerExclusive bool `json:"is_upower_exclusive"` + IsUpowerPlay bool `json:"is_upower_play"` + EnableVt int `json:"enable_vt"` + VtDisplay string `json:"vt_display"` + NoCache bool `json:"no_cache"` + Pages []struct { + Cid int `json:"cid"` + Page int `json:"page"` + From string `json:"from"` + Part string `json:"part"` + Duration int `json:"duration"` + Vid string `json:"vid"` + Weblink string `json:"weblink"` + Dimension struct { + Width int `json:"width"` + Height int `json:"height"` + Rotate int `json:"rotate"` + } `json:"dimension"` + FirstFrame string `json:"first_frame"` + } `json:"pages"` + Subtitle struct { + AllowSubmit bool `json:"allow_submit"` + List []interface{} `json:"list"` + } `json:"subtitle"` + UgcSeason struct { + ID int `json:"id"` + Title string `json:"title"` + Cover string `json:"cover"` + Mid int `json:"mid"` + Intro string `json:"intro"` + SignState int `json:"sign_state"` + Attribute int `json:"attribute"` + Sections []struct { + SeasonID int `json:"season_id"` + ID int `json:"id"` + Title string `json:"title"` + Type int `json:"type"` + Episodes []struct { + SeasonID int `json:"season_id"` + SectionID int `json:"section_id"` + ID int `json:"id"` + Aid int `json:"aid"` + Cid int `json:"cid"` + Title string `json:"title"` + Attribute int `json:"attribute"` + Arc struct { + Aid int `json:"aid"` + Videos int `json:"videos"` + TypeID int `json:"type_id"` + TypeName string `json:"type_name"` + Copyright int `json:"copyright"` + Pic string `json:"pic"` + Title string `json:"title"` + Pubdate int `json:"pubdate"` + Ctime int `json:"ctime"` + Desc string `json:"desc"` + State int `json:"state"` + Duration int `json:"duration"` + Rights struct { + Bp int `json:"bp"` + Elec int `json:"elec"` + Download int `json:"download"` + Movie int `json:"movie"` + Pay int `json:"pay"` + Hd5 int `json:"hd5"` + NoReprint int `json:"no_reprint"` + Autoplay int `json:"autoplay"` + UgcPay int `json:"ugc_pay"` + IsCooperation int `json:"is_cooperation"` + UgcPayPreview int `json:"ugc_pay_preview"` + ArcPay int `json:"arc_pay"` + FreeWatch int `json:"free_watch"` + } `json:"rights"` + Author struct { + Mid int `json:"mid"` + Name string `json:"name"` + Face string `json:"face"` + } `json:"author"` + Stat struct { + Aid int `json:"aid"` + View int `json:"view"` + Danmaku int `json:"danmaku"` + Reply int `json:"reply"` + Fav int `json:"fav"` + Coin int `json:"coin"` + Share int `json:"share"` + NowRank int `json:"now_rank"` + HisRank int `json:"his_rank"` + Like int `json:"like"` + Dislike int `json:"dislike"` + Evaluation string `json:"evaluation"` + ArgueMsg string `json:"argue_msg"` + Vt int `json:"vt"` + Vv int `json:"vv"` + } `json:"stat"` + Dynamic string `json:"dynamic"` + Dimension struct { + Width int `json:"width"` + Height int `json:"height"` + Rotate int `json:"rotate"` + } `json:"dimension"` + DescV2 interface{} `json:"desc_v2"` + IsChargeableSeason bool `json:"is_chargeable_season"` + IsBlooper bool `json:"is_blooper"` + EnableVt int `json:"enable_vt"` + VtDisplay string `json:"vt_display"` + } `json:"arc"` + Page struct { + Cid int `json:"cid"` + Page int `json:"page"` + From string `json:"from"` + Part string `json:"part"` + Duration int `json:"duration"` + Vid string `json:"vid"` + Weblink string `json:"weblink"` + Dimension struct { + Width int `json:"width"` + Height int `json:"height"` + Rotate int `json:"rotate"` + } `json:"dimension"` + } `json:"page"` + Bvid string `json:"bvid"` + } `json:"episodes"` + } `json:"sections"` + Stat struct { + SeasonID int `json:"season_id"` + View int `json:"view"` + Danmaku int `json:"danmaku"` + Reply int `json:"reply"` + Fav int `json:"fav"` + Coin int `json:"coin"` + Share int `json:"share"` + NowRank int `json:"now_rank"` + HisRank int `json:"his_rank"` + Like int `json:"like"` + Vt int `json:"vt"` + Vv int `json:"vv"` + } `json:"stat"` + EpCount int `json:"ep_count"` + SeasonType int `json:"season_type"` + IsPaySeason bool `json:"is_pay_season"` + EnableVt int `json:"enable_vt"` + } `json:"ugc_season"` + IsSeasonDisplay bool `json:"is_season_display"` + UserGarb struct { + URLImageAniCut string `json:"url_image_ani_cut"` + } `json:"user_garb"` + HonorReply struct { + Honor []struct { + Aid int `json:"aid"` + Type int `json:"type"` + Desc string `json:"desc"` + WeeklyRecommendNum int `json:"weekly_recommend_num"` + } `json:"honor"` + } `json:"honor_reply"` + LikeIcon string `json:"like_icon"` + NeedJumpBv bool `json:"need_jump_bv"` + DisableShowUpInfo bool `json:"disable_show_up_info"` + } `json:"data"` +} + +type videoInfo struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + From string `json:"from"` + Result string `json:"result"` + Message string `json:"message"` + Quality uint `json:"quality"` + Format string `json:"format"` + Timelength int `json:"timelength"` + AcceptFormat string `json:"accept_format"` + AcceptDescription []string `json:"accept_description"` + AcceptQuality []uint `json:"accept_quality"` + VideoCodecid int `json:"video_codecid"` + SeekParam string `json:"seek_param"` + SeekType string `json:"seek_type"` + Durl []struct { + Order int `json:"order"` + Length int `json:"length"` + Size int `json:"size"` + Ahead string `json:"ahead"` + Vhead string `json:"vhead"` + URL string `json:"url"` + BackupURL interface{} `json:"backup_url"` + } `json:"durl"` + SupportFormats []struct { + Quality int `json:"quality"` + Format string `json:"format"` + NewDescription string `json:"new_description"` + DisplayDesc string `json:"display_desc"` + Superscript string `json:"superscript"` + Codecs interface{} `json:"codecs"` + } `json:"support_formats"` + HighFormat interface{} `json:"high_format"` + LastPlayTime int `json:"last_play_time"` + LastPlayCid int `json:"last_play_cid"` + } `json:"data"` +} + +type playerV2Info struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + Aid int `json:"aid"` + Bvid string `json:"bvid"` + AllowBp bool `json:"allow_bp"` + NoShare bool `json:"no_share"` + Cid int `json:"cid"` + MaxLimit int `json:"max_limit"` + PageNo int `json:"page_no"` + HasNext bool `json:"has_next"` + IPInfo struct { + IP string `json:"ip"` + ZoneIP string `json:"zone_ip"` + ZoneID int `json:"zone_id"` + Country string `json:"country"` + Province string `json:"province"` + City string `json:"city"` + } `json:"ip_info"` + LoginMid int `json:"login_mid"` + LoginMidHash string `json:"login_mid_hash"` + IsOwner bool `json:"is_owner"` + Name string `json:"name"` + Permission string `json:"permission"` + LevelInfo struct { + CurrentLevel int `json:"current_level"` + CurrentMin int `json:"current_min"` + CurrentExp int `json:"current_exp"` + NextExp int `json:"next_exp"` + LevelUp int64 `json:"level_up"` + } `json:"level_info"` + Vip struct { + Type int `json:"type"` + Status int `json:"status"` + DueDate int64 `json:"due_date"` + VipPayType int `json:"vip_pay_type"` + ThemeType int `json:"theme_type"` + Label struct { + Path string `json:"path"` + Text string `json:"text"` + LabelTheme string `json:"label_theme"` + TextColor string `json:"text_color"` + BgStyle int `json:"bg_style"` + BgColor string `json:"bg_color"` + BorderColor string `json:"border_color"` + UseImgLabel bool `json:"use_img_label"` + ImgLabelURIHans string `json:"img_label_uri_hans"` + ImgLabelURIHant string `json:"img_label_uri_hant"` + ImgLabelURIHansStatic string `json:"img_label_uri_hans_static"` + ImgLabelURIHantStatic string `json:"img_label_uri_hant_static"` + } `json:"label"` + AvatarSubscript int `json:"avatar_subscript"` + NicknameColor string `json:"nickname_color"` + Role int `json:"role"` + AvatarSubscriptURL string `json:"avatar_subscript_url"` + TvVipStatus int `json:"tv_vip_status"` + TvVipPayType int `json:"tv_vip_pay_type"` + TvDueDate int `json:"tv_due_date"` + } `json:"vip"` + AnswerStatus int `json:"answer_status"` + BlockTime int `json:"block_time"` + Role string `json:"role"` + LastPlayTime int `json:"last_play_time"` + LastPlayCid int `json:"last_play_cid"` + NowTime int `json:"now_time"` + OnlineCount int `json:"online_count"` + NeedLoginSubtitle bool `json:"need_login_subtitle"` + Subtitle struct { + AllowSubmit bool `json:"allow_submit"` + Lan string `json:"lan"` + LanDoc string `json:"lan_doc"` + Subtitles []struct { + ID int64 `json:"id"` + Lan string `json:"lan"` + LanDoc string `json:"lan_doc"` + IsLock bool `json:"is_lock"` + SubtitleURL string `json:"subtitle_url"` + Type int `json:"type"` + IDStr string `json:"id_str"` + AiType int `json:"ai_type"` + AiStatus int `json:"ai_status"` + } `json:"subtitles"` + } `json:"subtitle"` + PlayerIcon struct { + URL1 string `json:"url1"` + Hash1 string `json:"hash1"` + URL2 string `json:"url2"` + Hash2 string `json:"hash2"` + Ctime int `json:"ctime"` + } `json:"player_icon"` + ViewPoints []interface{} `json:"view_points"` + IsUgcPayPreview bool `json:"is_ugc_pay_preview"` + PreviewToast string `json:"preview_toast"` + Options struct { + Is360 bool `json:"is_360"` + WithoutVip bool `json:"without_vip"` + } `json:"options"` + GuideAttention []interface{} `json:"guide_attention"` + JumpCard []interface{} `json:"jump_card"` + OperationCard []interface{} `json:"operation_card"` + OnlineSwitch struct { + EnableGrayDashPlayback string `json:"enable_gray_dash_playback"` + NewBroadcast string `json:"new_broadcast"` + RealtimeDm string `json:"realtime_dm"` + SubtitleSubmitSwitch string `json:"subtitle_submit_switch"` + } `json:"online_switch"` + Fawkes struct { + ConfigVersion int `json:"config_version"` + FfVersion int `json:"ff_version"` + } `json:"fawkes"` + ShowSwitch struct { + LongProgress bool `json:"long_progress"` + } `json:"show_switch"` + BgmInfo interface{} `json:"bgm_info"` + ToastBlock bool `json:"toast_block"` + IsUpowerExclusive bool `json:"is_upower_exclusive"` + IsUpowerPlay bool `json:"is_upower_play"` + ElecHighLevel struct { + PrivilegeType int `json:"privilege_type"` + LevelStr string `json:"level_str"` + Title string `json:"title"` + Intro string `json:"intro"` + } `json:"elec_high_level"` + DisableShowUpInfo bool `json:"disable_show_up_info"` + } `json:"data"` +} + +type seasonInfo struct { + Code int `json:"code"` + Message string `json:"message"` + Result struct { + Activity struct { + HeadBgURL string `json:"head_bg_url"` + ID int `json:"id"` + Title string `json:"title"` + } `json:"activity"` + Actors string `json:"actors"` + Alias string `json:"alias"` + Areas []struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"areas"` + BkgCover string `json:"bkg_cover"` + Cover string `json:"cover"` + EnableVt bool `json:"enable_vt"` + Episodes []struct { + Aid int `json:"aid"` + Badge string `json:"badge"` + BadgeInfo struct { + BgColor string `json:"bg_color"` + BgColorNight string `json:"bg_color_night"` + Text string `json:"text"` + } `json:"badge_info"` + BadgeType int `json:"badge_type"` + Bvid string `json:"bvid"` + Cid uint `json:"cid"` + Cover string `json:"cover"` + Dimension struct { + Height int `json:"height"` + Rotate int `json:"rotate"` + Width int `json:"width"` + } `json:"dimension"` + Duration int `json:"duration"` + EnableVt bool `json:"enable_vt"` + EpID uint `json:"ep_id"` + From string `json:"from"` + ID int `json:"id"` + IsViewHide bool `json:"is_view_hide"` + Link string `json:"link"` + LongTitle string `json:"long_title"` + PubTime int `json:"pub_time"` + Pv int `json:"pv"` + ReleaseDate string `json:"release_date"` + Rights struct { + AllowDemand int `json:"allow_demand"` + AllowDm int `json:"allow_dm"` + AllowDownload int `json:"allow_download"` + AreaLimit int `json:"area_limit"` + } `json:"rights"` + ShareCopy string `json:"share_copy"` + ShareURL string `json:"share_url"` + ShortLink string `json:"short_link"` + ShowDrmLoginDialog bool `json:"showDrmLoginDialog"` + Skip struct { + Ed struct { + End int `json:"end"` + Start int `json:"start"` + } `json:"ed"` + Op struct { + End int `json:"end"` + Start int `json:"start"` + } `json:"op"` + } `json:"skip"` + Status int `json:"status"` + Subtitle string `json:"subtitle"` + Title string `json:"title"` + Vid string `json:"vid"` + } `json:"episodes"` + Evaluate string `json:"evaluate"` + Freya struct { + BubbleDesc string `json:"bubble_desc"` + BubbleShowCnt int `json:"bubble_show_cnt"` + IconShow int `json:"icon_show"` + } `json:"freya"` + IconFont struct { + Name string `json:"name"` + Text string `json:"text"` + } `json:"icon_font"` + JpTitle string `json:"jp_title"` + Link string `json:"link"` + MediaID int `json:"media_id"` + Mode int `json:"mode"` + NewEp struct { + Desc string `json:"desc"` + ID int `json:"id"` + IsNew int `json:"is_new"` + Title string `json:"title"` + } `json:"new_ep"` + Payment struct { + Discount int `json:"discount"` + PayType struct { + AllowDiscount int `json:"allow_discount"` + AllowPack int `json:"allow_pack"` + AllowTicket int `json:"allow_ticket"` + AllowTimeLimit int `json:"allow_time_limit"` + AllowVipDiscount int `json:"allow_vip_discount"` + ForbidBb int `json:"forbid_bb"` + } `json:"pay_type"` + Price string `json:"price"` + Promotion string `json:"promotion"` + Tip string `json:"tip"` + ViewStartTime int `json:"view_start_time"` + VipDiscount int `json:"vip_discount"` + VipFirstPromotion string `json:"vip_first_promotion"` + VipPrice string `json:"vip_price"` + VipPromotion string `json:"vip_promotion"` + } `json:"payment"` + PlayStrategy struct { + Strategies []string `json:"strategies"` + } `json:"play_strategy"` + Positive struct { + ID int `json:"id"` + Title string `json:"title"` + } `json:"positive"` + Publish struct { + IsFinish int `json:"is_finish"` + IsStarted int `json:"is_started"` + PubTime string `json:"pub_time"` + PubTimeShow string `json:"pub_time_show"` + UnknowPubDate int `json:"unknow_pub_date"` + Weekday int `json:"weekday"` + } `json:"publish"` + Rating struct { + Count int `json:"count"` + Score float64 `json:"score"` + } `json:"rating"` + Record string `json:"record"` + Rights struct { + AllowBp int `json:"allow_bp"` + AllowBpRank int `json:"allow_bp_rank"` + AllowDownload int `json:"allow_download"` + AllowReview int `json:"allow_review"` + AreaLimit int `json:"area_limit"` + BanAreaShow int `json:"ban_area_show"` + CanWatch int `json:"can_watch"` + Copyright string `json:"copyright"` + ForbidPre int `json:"forbid_pre"` + FreyaWhite int `json:"freya_white"` + IsCoverShow int `json:"is_cover_show"` + IsPreview int `json:"is_preview"` + OnlyVipDownload int `json:"only_vip_download"` + Resource string `json:"resource"` + WatchPlatform int `json:"watch_platform"` + } `json:"rights"` + SeasonID int `json:"season_id"` + SeasonTitle string `json:"season_title"` + Seasons []struct { + Badge string `json:"badge"` + BadgeInfo struct { + BgColor string `json:"bg_color"` + BgColorNight string `json:"bg_color_night"` + Text string `json:"text"` + } `json:"badge_info"` + BadgeType int `json:"badge_type"` + Cover string `json:"cover"` + EnableVt bool `json:"enable_vt"` + HorizontalCover1610 string `json:"horizontal_cover_1610"` + HorizontalCover169 string `json:"horizontal_cover_169"` + IconFont struct { + Name string `json:"name"` + Text string `json:"text"` + } `json:"icon_font"` + MediaID int `json:"media_id"` + NewEp struct { + Cover string `json:"cover"` + ID int `json:"id"` + IndexShow string `json:"index_show"` + } `json:"new_ep"` + SeasonID int `json:"season_id"` + SeasonTitle string `json:"season_title"` + SeasonType int `json:"season_type"` + Stat struct { + Favorites int `json:"favorites"` + SeriesFollow int `json:"series_follow"` + Views int `json:"views"` + Vt int `json:"vt"` + } `json:"stat"` + } `json:"seasons"` + Section []struct { + Attr int `json:"attr"` + EpisodeID int `json:"episode_id"` + EpisodeIds []interface{} `json:"episode_ids"` + Episodes []struct { + Aid int `json:"aid"` + Badge string `json:"badge"` + BadgeInfo struct { + BgColor string `json:"bg_color"` + BgColorNight string `json:"bg_color_night"` + Text string `json:"text"` + } `json:"badge_info"` + BadgeType int `json:"badge_type"` + Bvid string `json:"bvid"` + Cid int `json:"cid"` + Cover string `json:"cover"` + Dimension struct { + Height int `json:"height"` + Rotate int `json:"rotate"` + Width int `json:"width"` + } `json:"dimension"` + Duration int `json:"duration"` + EnableVt bool `json:"enable_vt"` + EpID int `json:"ep_id"` + From string `json:"from"` + IconFont struct { + Name string `json:"name"` + Text string `json:"text"` + } `json:"icon_font"` + ID int `json:"id"` + IsViewHide bool `json:"is_view_hide"` + Link string `json:"link"` + LongTitle string `json:"long_title"` + PubTime int `json:"pub_time"` + Pv int `json:"pv"` + ReleaseDate string `json:"release_date"` + Rights struct { + AllowDemand int `json:"allow_demand"` + AllowDm int `json:"allow_dm"` + AllowDownload int `json:"allow_download"` + AreaLimit int `json:"area_limit"` + } `json:"rights"` + ShareCopy string `json:"share_copy"` + ShareURL string `json:"share_url"` + ShortLink string `json:"short_link"` + ShowDrmLoginDialog bool `json:"showDrmLoginDialog"` + Skip struct { + Ed struct { + End int `json:"end"` + Start int `json:"start"` + } `json:"ed"` + Op struct { + End int `json:"end"` + Start int `json:"start"` + } `json:"op"` + } `json:"skip"` + Stat struct { + Coin int `json:"coin"` + Danmakus int `json:"danmakus"` + Likes int `json:"likes"` + Play int `json:"play"` + Reply int `json:"reply"` + Vt int `json:"vt"` + } `json:"stat"` + StatForUnity struct { + Coin int `json:"coin"` + Danmaku struct { + Icon string `json:"icon"` + PureText string `json:"pure_text"` + Text string `json:"text"` + Value int `json:"value"` + } `json:"danmaku"` + Likes int `json:"likes"` + Reply int `json:"reply"` + Vt struct { + Icon string `json:"icon"` + PureText string `json:"pure_text"` + Text string `json:"text"` + Value int `json:"value"` + } `json:"vt"` + } `json:"stat_for_unity"` + Status int `json:"status"` + Subtitle string `json:"subtitle"` + Title string `json:"title"` + Vid string `json:"vid"` + } `json:"episodes"` + ID int `json:"id"` + Title string `json:"title"` + Type int `json:"type"` + Type2 int `json:"type2"` + } `json:"section"` + Series struct { + DisplayType int `json:"display_type"` + SeriesID int `json:"series_id"` + SeriesTitle string `json:"series_title"` + } `json:"series"` + ShareCopy string `json:"share_copy"` + ShareSubTitle string `json:"share_sub_title"` + ShareURL string `json:"share_url"` + Show struct { + WideScreen int `json:"wide_screen"` + } `json:"show"` + ShowSeasonType int `json:"show_season_type"` + SquareCover string `json:"square_cover"` + Staff string `json:"staff"` + Stat struct { + Coins int `json:"coins"` + Danmakus int `json:"danmakus"` + Favorite int `json:"favorite"` + Favorites int `json:"favorites"` + FollowText string `json:"follow_text"` + Likes int `json:"likes"` + Reply int `json:"reply"` + Share int `json:"share"` + Views int `json:"views"` + Vt int `json:"vt"` + } `json:"stat"` + Status int `json:"status"` + Styles []string `json:"styles"` + Subtitle string `json:"subtitle"` + Title string `json:"title"` + Total int `json:"total"` + Type int `json:"type"` + UpInfo struct { + Avatar string `json:"avatar"` + AvatarSubscriptURL string `json:"avatar_subscript_url"` + Follower int `json:"follower"` + IsFollow int `json:"is_follow"` + Mid int `json:"mid"` + NicknameColor string `json:"nickname_color"` + Pendant struct { + Image string `json:"image"` + Name string `json:"name"` + Pid int `json:"pid"` + } `json:"pendant"` + ThemeType int `json:"theme_type"` + Uname string `json:"uname"` + VerifyType int `json:"verify_type"` + VipLabel struct { + BgColor string `json:"bg_color"` + BgStyle int `json:"bg_style"` + BorderColor string `json:"border_color"` + Text string `json:"text"` + TextColor string `json:"text_color"` + } `json:"vip_label"` + VipStatus int `json:"vip_status"` + VipType int `json:"vip_type"` + } `json:"up_info"` + UserStatus struct { + AreaLimit int `json:"area_limit"` + BanAreaShow int `json:"ban_area_show"` + Follow int `json:"follow"` + FollowStatus int `json:"follow_status"` + Login int `json:"login"` + Pay int `json:"pay"` + PayPackPaid int `json:"pay_pack_paid"` + Sponsor int `json:"sponsor"` + } `json:"user_status"` + } `json:"result"` +} + +type pgcURLInfo struct { + Code int `json:"code"` + Message string `json:"message"` + Result struct { + AcceptFormat string `json:"accept_format"` + Code int `json:"code"` + SeekParam string `json:"seek_param"` + IsPreview int `json:"is_preview"` + Fnval int `json:"fnval"` + VideoProject bool `json:"video_project"` + Fnver int `json:"fnver"` + Type string `json:"type"` + Bp int `json:"bp"` + Result string `json:"result"` + SeekType string `json:"seek_type"` + From string `json:"from"` + VideoCodecid int `json:"video_codecid"` + RecordInfo struct { + RecordIcon string `json:"record_icon"` + Record string `json:"record"` + } `json:"record_info"` + Durl []struct { + Size int `json:"size"` + Ahead string `json:"ahead"` + Length int `json:"length"` + Vhead string `json:"vhead"` + BackupURL []string `json:"backup_url"` + URL string `json:"url"` + Order int `json:"order"` + Md5 string `json:"md5"` + } `json:"durl"` + IsDrm bool `json:"is_drm"` + NoRexcode int `json:"no_rexcode"` + Format string `json:"format"` + SupportFormats []struct { + DisplayDesc string `json:"display_desc"` + Superscript string `json:"superscript"` + NeedLogin bool `json:"need_login,omitempty"` + Codecs []interface{} `json:"codecs"` + Format string `json:"format"` + Description string `json:"description"` + Quality int `json:"quality"` + NewDescription string `json:"new_description"` + } `json:"support_formats"` + Message string `json:"message"` + AcceptQuality []uint `json:"accept_quality"` + Quality uint `json:"quality"` + Timelength int `json:"timelength"` + HasPaid bool `json:"has_paid"` + ClipInfoList []interface{} `json:"clip_info_list"` + AcceptDescription []string `json:"accept_description"` + Status int `json:"status"` + } `json:"result"` +} diff --git a/vendors/bilibili/bilibili_test.go b/vendors/bilibili/bilibili_test.go new file mode 100644 index 0000000..8f1ab28 --- /dev/null +++ b/vendors/bilibili/bilibili_test.go @@ -0,0 +1,64 @@ +package bilibili_test + +import ( + "testing" + + "github.com/synctv-org/synctv/vendors/bilibili" +) + +func TestMatch(t *testing.T) { + t.Parallel() + tests := []struct { + name string + url string + wantT string + wantID string + wantErr bool + }{ + { + name: "bv", + url: "https://www.bilibili.com/video/BV1i5411y7fB", + wantT: "bv", + wantID: "BV1i5411y7fB", + wantErr: false, + }, + { + name: "av", + url: "https://www.bilibili.com/video/av1", + wantT: "av", + wantID: "1", + wantErr: false, + }, + { + name: "ss", + url: "https://www.bilibili.com/bangumi/play/ss1", + wantT: "ss", + wantID: "1", + wantErr: false, + }, + { + name: "ep", + url: "https://www.bilibili.com/bangumi/play/ep1", + wantT: "ep", + wantID: "1", + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotT, gotID, err := bilibili.Match(tt.url) + if (err != nil) != tt.wantErr { + t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotT != tt.wantT { + t.Errorf("Match() gotT = %v, want %v", gotT, tt.wantT) + } + if gotID != tt.wantID { + t.Errorf("Match() gotID = %v, want %v", gotID, tt.wantID) + } + }) + } +} diff --git a/vendors/bilibili/client.go b/vendors/bilibili/client.go new file mode 100644 index 0000000..34838f6 --- /dev/null +++ b/vendors/bilibili/client.go @@ -0,0 +1,45 @@ +package bilibili + +import ( + "io" + "net/http" + + "github.com/synctv-org/synctv/utils" +) + +type Client struct { + httpClient *http.Client + cookies []*http.Cookie +} + +type ClientConfig func(*Client) + +func WithHttpClient(httpClient *http.Client) ClientConfig { + return func(c *Client) { + c.httpClient = httpClient + } +} + +func NewClient(cookies []*http.Cookie, conf ...ClientConfig) *Client { + c := &Client{ + httpClient: http.DefaultClient, + cookies: cookies, + } + for _, v := range conf { + v(c) + } + return c +} + +func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + for _, cookie := range c.cookies { + req.AddCookie(cookie) + } + req.Header.Set("User-Agent", utils.UA) + req.Header.Set("Referer", "https://www.bilibili.com/") + return req, nil +} diff --git a/vendors/bilibili/login.go b/vendors/bilibili/login.go new file mode 100644 index 0000000..c4f1e22 --- /dev/null +++ b/vendors/bilibili/login.go @@ -0,0 +1,46 @@ +package bilibili + +import ( + "fmt" + "net/http" + + json "github.com/json-iterator/go" +) + +type RQCode struct { + URL string `json:"url"` + Key string `json:"key"` +} + +func NewQRCode() (*RQCode, error) { + resp, err := http.Get("https://passport.bilibili.com/x/passport-login/web/qrcode/generate") + if err != nil { + return nil, err + } + defer resp.Body.Close() + qr := qrcodeResp{} + err = json.NewDecoder(resp.Body).Decode(&qr) + if err != nil { + return nil, err + } + // TODO: error message + return &RQCode{ + URL: qr.Data.URL, + Key: qr.Data.QrcodeKey, + }, nil +} + +// return SESSDATA cookie +func Login(key string) (*http.Cookie, error) { + resp, err := http.Get(fmt.Sprintf("https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=%s", key)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + for _, cookie := range resp.Cookies() { + if cookie.Name == "SESSDATA" { + return cookie, nil + } + } + return nil, fmt.Errorf("no SESSDATA cookie") +} diff --git a/vendors/bilibili/movie.go b/vendors/bilibili/movie.go new file mode 100644 index 0000000..42fc083 --- /dev/null +++ b/vendors/bilibili/movie.go @@ -0,0 +1,252 @@ +package bilibili + +import ( + "fmt" + "net/http" + + json "github.com/json-iterator/go" +) + +type VideoPageInfo struct { + Bvid string `json:"bvid"` + Title string `json:"title"` + CoverImage string `json:"coverImage"` + VideoInfos []*VideoInfo `json:"videoInfos"` +} + +type VideoInfo struct { + Cid int `json:"cid"` + // 分P + Name string `json:"name"` + FirstFrame string `json:"firstFrame"` +} + +func (c *Client) ParseVideoPage(aid uint, bvid string) (*VideoPageInfo, error) { + var url string + if aid != 0 { + url = fmt.Sprintf("https://api.bilibili.com/x/web-interface/view?aid=%d", aid) + } else if bvid != "" { + url = fmt.Sprintf("https://api.bilibili.com/x/web-interface/view?bvid=%s", bvid) + } else { + return nil, fmt.Errorf("aid and bvid are both empty") + } + req, err := c.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + info := videoPageInfo{} + err = json.NewDecoder(resp.Body).Decode(&info) + if err != nil { + return nil, err + } + // TODO: error message + r := &VideoPageInfo{ + Bvid: info.Data.Bvid, + Title: info.Data.Title, + CoverImage: info.Data.Pic, + } + r.VideoInfos = make([]*VideoInfo, 0, len(info.Data.Pages)) + for _, page := range info.Data.Pages { + r.VideoInfos = append(r.VideoInfos, &VideoInfo{ + Cid: page.Cid, + Name: page.Part, + FirstFrame: page.FirstFrame, + }) + } + return r, nil +} + +const ( + Q240P uint = 6 + Q360P uint = 16 + Q480P uint = 32 + Q720P uint = 64 + Q1080P uint = 80 + Q1080PP uint = 112 + Q1080P60 uint = 116 + Q4K uint = 120 + QHDR uint = 124 + QDOLBY uint = 126 + Q8K uint = 127 +) + +type VideoURL struct { + AcceptDescription []string `json:"acceptDescription"` + AcceptQuality []uint `json:"acceptQuality"` + CurrentQuality uint `json:"currentQuality"` + URL string `json:"url"` +} + +type GetVideoURLConf struct { + Quality uint +} + +type GetVideoURLConfig func(*GetVideoURLConf) + +// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md +func (c *Client) GetVideoURL(aid uint, bvid string, cid uint, conf ...GetVideoURLConfig) (*VideoURL, error) { + config := &GetVideoURLConf{ + Quality: Q1080PP, + } + for _, v := range conf { + v(config) + } + var url string + if aid != 0 { + url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?aid=%d&cid=%d&qn=%d&platform=html5&high_quality=1", aid, cid, config.Quality) + } else if bvid != "" { + url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?bvid=%s&cid=%d&qn=%d&platform=html5&high_quality=1", bvid, cid, config.Quality) + } else { + return nil, fmt.Errorf("aid and bvid are both empty") + } + req, err := c.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + info := videoInfo{} + err = json.NewDecoder(resp.Body).Decode(&info) + if err != nil { + return nil, err + } + return &VideoURL{ + AcceptDescription: info.Data.AcceptDescription, + AcceptQuality: info.Data.AcceptQuality, + CurrentQuality: info.Data.Quality, + URL: info.Data.Durl[0].URL, + }, nil +} + +type Subtitle struct { + Name string `json:"name"` + URL string `json:"url"` +} + +func (c *Client) GetSubtitles(aid uint, bvid string, cid uint) ([]*Subtitle, error) { + var url string + if aid != 0 { + url = fmt.Sprintf("https://api.bilibili.com/x/player/v2?aid=%d&cid=%d", aid, cid) + } else if bvid != "" { + url = fmt.Sprintf("https://api.bilibili.com/x/player/v2?bvid=%s&cid=%d", bvid, cid) + } else { + return nil, fmt.Errorf("aid and bvid are both empty") + } + req, err := c.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + info := playerV2Info{} + err = json.NewDecoder(resp.Body).Decode(&info) + if err != nil { + return nil, err + } + r := make([]*Subtitle, len(info.Data.Subtitle.Subtitles)) + for i, s := range info.Data.Subtitle.Subtitles { + r[i] = &Subtitle{ + Name: s.LanDoc, + URL: s.SubtitleURL, + } + } + return r, nil +} + +type PGCPageInfo struct { + Actors string `json:"actors"` + CoverImage string `json:"coverImage"` + PGCInfos []*PGCInfo `json:"pgcInfos"` +} + +type PGCInfo struct { + EpId uint `json:"epId"` + Cid uint `json:"cid"` + Name string `json:"name"` + CoverImage string `json:"coverImage"` +} + +func (c *Client) ParsePGCPage(epId, season_id uint) (*PGCPageInfo, error) { + var url string + if epId != 0 { + url = fmt.Sprintf("https://api.bilibili.com/pgc/view/web/season?ep_id=%d", epId) + } else if season_id != 0 { + url = fmt.Sprintf("https://api.bilibili.com/pgc/view/web/season?season_id=%d", season_id) + } else { + return nil, fmt.Errorf("edId and season_id are both empty") + } + + req, err := c.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + info := seasonInfo{} + err = json.NewDecoder(resp.Body).Decode(&info) + if err != nil { + return nil, err + } + + r := &PGCPageInfo{ + Actors: info.Result.Actors, + CoverImage: info.Result.Cover, + PGCInfos: make([]*PGCInfo, len(info.Result.Episodes)), + } + + for i, v := range info.Result.Episodes { + r.PGCInfos[i] = &PGCInfo{ + EpId: v.EpID, + Cid: v.Cid, + Name: v.ShareCopy, + CoverImage: v.Cover, + } + } + + return r, nil +} + +func (c *Client) GetPGCURL(ep_id, cid uint, conf ...GetVideoURLConfig) (*VideoURL, error) { + config := &GetVideoURLConf{ + Quality: Q1080PP, + } + for _, v := range conf { + v(config) + } + url := fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?ep_id=%d&cid=%d&qn=%d&fourk=1", ep_id, cid, config.Quality) + req, err := c.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + info := pgcURLInfo{} + err = json.NewDecoder(resp.Body).Decode(&info) + if err != nil { + return nil, err + } + return &VideoURL{ + AcceptDescription: info.Result.AcceptDescription, + AcceptQuality: info.Result.AcceptQuality, + CurrentQuality: info.Result.Quality, + URL: info.Result.Durl[0].URL, + }, nil + +} diff --git a/vendors/bilibili/utils.go b/vendors/bilibili/utils.go new file mode 100644 index 0000000..98a5c0b --- /dev/null +++ b/vendors/bilibili/utils.go @@ -0,0 +1,29 @@ +package bilibili + +import ( + "errors" + "regexp" +) + +var ( + BVRegex = regexp.MustCompile(`(?:https://www.bilibili.com/video/)?((?:bv|bV|Bv|BV)\w+)(?:/(\?.*)?)?$`) + ARegex = regexp.MustCompile(`(?:https://www.bilibili.com/video/)?(?:av|aV|Av|AV)(\d+)(?:/(\?.*)?)?$`) + SSRegex = regexp.MustCompile(`(?:https://www.bilibili.com/bangumi/play/)?(?:ss|sS|Ss|SS)(\d+)(?:\?.*)?$`) + EPRegex = regexp.MustCompile(`(?:https://www.bilibili.com/bangumi/play/)?(?:ep|eP|Ep|EP)(\d+)(?:\?.*)?$`) +) + +func Match(url string) (t string, id string, err error) { + if m := BVRegex.FindStringSubmatch(url); m != nil { + return "bv", m[1], nil + } + if m := ARegex.FindStringSubmatch(url); m != nil { + return "av", m[1], nil + } + if m := SSRegex.FindStringSubmatch(url); m != nil { + return "ss", m[1], nil + } + if m := EPRegex.FindStringSubmatch(url); m != nil { + return "ep", m[1], nil + } + return "", "", errors.New("match failed") +}