Refector: movie list support folder (#153) (#154)

* Refector: movie list support folder

* Opt: alist sub path use relative paths

* Feat: emby dynamic folder

* Feat: gen sub path

* Opt: the number of deleteMovieAndChiledCache calls

* Fix: need update database

* Fix: the wrong user obj was used for list dynamic folder load cache

* Fix: secret list dynamic folder

* Fix: update or delete movie need check current movie is child

* Fix: sub path join is url encode, can not check prefix

* Opt: clear movie need parentId
pull/161/head
zijiren 2 years ago committed by GitHub
parent 6ce80d1aec
commit e1294063a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"path"
"strings"
"time"
@ -80,8 +81,8 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali
type AlistMovieCache = refreshcache.RefreshCache[*AlistMovieCacheData, *AlistMovieCacheFuncArgs]
func NewAlistMovieCache(movie *model.Movie) *AlistMovieCache {
return refreshcache.NewRefreshCache(NewAlistMovieCacheInitFunc(movie), time.Minute*14)
func NewAlistMovieCache(movie *model.Movie, subPath string) *AlistMovieCache {
return refreshcache.NewRefreshCache(NewAlistMovieCacheInitFunc(movie, subPath), time.Minute*14)
}
type AlistProvider = string
@ -161,7 +162,7 @@ type AlistMovieCacheFuncArgs struct {
UserAgent string
}
func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, args ...*AlistMovieCacheFuncArgs) (*AlistMovieCacheData, error) {
func NewAlistMovieCacheInitFunc(movie *model.Movie, subPath string) func(ctx context.Context, args ...*AlistMovieCacheFuncArgs) (*AlistMovieCacheData, error) {
return func(ctx context.Context, args ...*AlistMovieCacheFuncArgs) (*AlistMovieCacheData, error) {
if len(args) == 0 {
return nil, errors.New("need alist user cache")
@ -170,15 +171,26 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar
if userCache == nil {
return nil, errors.New("need alist user cache")
}
if movie.IsFolder && subPath == "" {
return nil, errors.New("sub path is empty")
}
var (
serverID string
err error
truePath string
)
serverID, truePath, err = model.GetAlistServerIdFromPath(movie.Base.VendorInfo.Alist.Path)
serverID, truePath, err = movie.MovieBase.VendorInfo.Alist.ServerIDAndFilePath()
if err != nil {
return nil, err
}
if movie.IsFolder {
newPath := path.Join(truePath, subPath)
// check new path is in parent path
if !strings.HasPrefix(newPath, truePath) {
return nil, errors.New("sub path is not in parent path")
}
truePath = newPath
}
aucd, err := userCache.LoadOrStore(ctx, serverID)
if err != nil {
return nil, err
@ -186,12 +198,12 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar
if aucd.Host == "" {
return nil, errors.New("not bind alist vendor")
}
cli := vendor.LoadAlistClient(movie.Base.VendorInfo.Backend)
cli := vendor.LoadAlistClient(movie.MovieBase.VendorInfo.Backend)
fg, err := cli.FsGet(ctx, &alist.FsGetReq{
Host: aucd.Host,
Token: aucd.Token,
Path: truePath,
Password: movie.Base.VendorInfo.Alist.Password,
Password: movie.MovieBase.VendorInfo.Alist.Password,
UserAgent: args[0].UserAgent,
})
if err != nil {
@ -214,7 +226,7 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar
Host: aucd.Host,
Token: aucd.Token,
Path: prefix + related.Name,
Password: movie.Base.VendorInfo.Alist.Password,
Password: movie.MovieBase.VendorInfo.Alist.Password,
UserAgent: args[0].UserAgent,
})
if err != nil {
@ -248,7 +260,7 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar
Host: aucd.Host,
Token: aucd.Token,
Path: truePath,
Password: movie.Base.VendorInfo.Alist.Password,
Password: movie.MovieBase.VendorInfo.Alist.Password,
Method: "video_preview",
})
if err != nil {

@ -50,9 +50,9 @@ func BilibiliSharedMpdCacheInitFunc(ctx context.Context, movie *model.Movie, arg
} else {
cookies = vendorInfo.Cookies
}
cli := vendor.LoadBilibiliClient(movie.Base.VendorInfo.Backend)
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
var m, hevcM *mpd.MPD
biliInfo := movie.Base.VendorInfo.Bilibili
biliInfo := movie.MovieBase.VendorInfo.Bilibili
switch {
case biliInfo.Epid != 0:
resp, err := cli.GetDashPGCURL(ctx, &bilibili.GetDashPGCURLReq{
@ -177,9 +177,9 @@ func BilibiliNoSharedMovieCacheInitFunc(ctx context.Context, movie *model.Movie,
} else {
cookies = vendorInfo.Cookies
}
cli := vendor.LoadBilibiliClient(movie.Base.VendorInfo.Backend)
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
var u string
biliInfo := movie.Base.VendorInfo.Bilibili
biliInfo := movie.MovieBase.VendorInfo.Bilibili
switch {
case biliInfo.Epid != 0:
resp, err := cli.GetPGCURL(ctx, &bilibili.GetPGCURLReq{
@ -239,7 +239,7 @@ func BilibiliSubtitleCacheInitFunc(ctx context.Context, movie *model.Movie, args
return nil, errors.New("no bilibili user cache data")
}
biliInfo := movie.Base.VendorInfo.Bilibili
biliInfo := movie.MovieBase.VendorInfo.Bilibili
if biliInfo.Bvid == "" || biliInfo.Cid == 0 {
return nil, errors.New("bvid or cid is empty")
}
@ -255,7 +255,7 @@ func BilibiliSubtitleCacheInitFunc(ctx context.Context, movie *model.Movie, args
cookies = vendorInfo.Cookies
}
cli := vendor.LoadBilibiliClient(movie.Base.VendorInfo.Backend)
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
resp, err := cli.GetSubtitles(ctx, &bilibili.GetSubtitlesReq{
Cookies: utils.HttpCookieToMap(cookies),
Bvid: biliInfo.Bvid,
@ -348,9 +348,9 @@ func genBilibiliLiveM3U8ListFile(urls []*bilibili.LiveStream) []byte {
}
func BilibiliLiveCacheInitFunc(ctx context.Context, movie *model.Movie, args ...struct{}) ([]byte, error) {
cli := vendor.LoadBilibiliClient(movie.Base.VendorInfo.Backend)
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
resp, err := cli.GetLiveStreams(ctx, &bilibili.GetLiveStreamsReq{
Cid: movie.Base.VendorInfo.Bilibili.Cid,
Cid: movie.MovieBase.VendorInfo.Bilibili.Cid,
Hls: true,
})
if err != nil {

@ -73,19 +73,22 @@ type EmbyMovieCacheData struct {
type EmbyMovieCache = refreshcache.RefreshCache[*EmbyMovieCacheData, *EmbyUserCache]
func NewEmbyMovieCache(movie *model.Movie) *EmbyMovieCache {
cache := refreshcache.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie), 0)
cache.SetClearFunc(NewEmbyMovieClearCacheFunc(movie))
func NewEmbyMovieCache(movie *model.Movie, subPath string) *EmbyMovieCache {
cache := refreshcache.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie, subPath), 0)
cache.SetClearFunc(NewEmbyMovieClearCacheFunc(movie, subPath))
return cache
}
func NewEmbyMovieClearCacheFunc(movie *model.Movie) func(ctx context.Context, args ...*EmbyUserCache) error {
func NewEmbyMovieClearCacheFunc(movie *model.Movie, subPath string) func(ctx context.Context, args ...*EmbyUserCache) error {
return func(ctx context.Context, args ...*MapCache[*EmbyUserCacheData, struct{}]) error {
if !movie.Base.VendorInfo.Emby.Transcode {
if !movie.MovieBase.VendorInfo.Emby.Transcode {
return nil
}
if len(args) == 0 {
return errors.New("need emby user cache")
}
serverID, _, err := model.GetEmbyServerIdFromPath(movie.Base.VendorInfo.Emby.Path)
serverID, err := movie.MovieBase.VendorInfo.Emby.ServerID()
if err != nil {
return err
}
@ -115,20 +118,26 @@ func NewEmbyMovieClearCacheFunc(movie *model.Movie) func(ctx context.Context, ar
}
}
func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, args ...*EmbyUserCache) (*EmbyMovieCacheData, error) {
func NewEmbyMovieCacheInitFunc(movie *model.Movie, subPath string) func(ctx context.Context, args ...*EmbyUserCache) (*EmbyMovieCacheData, error) {
return func(ctx context.Context, args ...*EmbyUserCache) (*EmbyMovieCacheData, error) {
if len(args) == 0 {
return nil, errors.New("need emby user cache")
}
if movie.IsFolder && subPath == "" {
return nil, errors.New("sub path is empty")
}
var (
serverID string
err error
truePath string
)
serverID, truePath, err = model.GetEmbyServerIdFromPath(movie.Base.VendorInfo.Emby.Path)
serverID, truePath, err = movie.MovieBase.VendorInfo.Emby.ServerIDAndFilePath()
if err != nil {
return nil, err
}
if movie.IsFolder {
truePath = subPath
}
aucd, err := args[0].LoadOrStore(ctx, serverID)
if err != nil {
@ -156,7 +165,7 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
return nil, err
}
for i, v := range data.MediaSourceInfo {
if movie.Base.VendorInfo.Emby.Transcode && v.TranscodingUrl != "" {
if movie.MovieBase.VendorInfo.Emby.Transcode && v.TranscodingUrl != "" {
resp.Sources[i].URL = fmt.Sprintf("%s/emby%s", aucd.Host, v.TranscodingUrl)
resp.Sources[i].IsTranscode = true
resp.Sources[i].Name = v.Name

@ -16,10 +16,31 @@ func CreateMovies(movies []*model.Movie) error {
})
}
func GetAllMoviesByRoomID(roomID string) []*model.Movie {
func WithParentMovieID(parentMovieID string) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if parentMovieID == "" {
return db.Where("base_parent_id IS NULL")
}
return db.Where("base_parent_id = ?", parentMovieID)
}
}
func GetMoviesByRoomID(roomID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.Movie, error) {
movies := []*model.Movie{}
db.Where("room_id = ?", roomID).Order("position ASC").Find(&movies)
return movies
err := db.Where("room_id = ?", roomID).Order("position ASC").Scopes(scopes...).Find(&movies).Error
return movies, err
}
func GetMoviesCountByRoomID(roomID string, scopes ...func(*gorm.DB) *gorm.DB) (int64, error) {
var count int64
err := db.Model(&model.Movie{}).Where("room_id = ?", roomID).Scopes(scopes...).Count(&count).Error
return count, err
}
func GetMovieByID(roomID, id string, scopes ...func(*gorm.DB) *gorm.DB) (*model.Movie, error) {
movie := &model.Movie{}
err := db.Where("room_id = ? AND id = ?", roomID, id).Scopes(scopes...).First(movie).Error
return movie, HandleNotFound(err, "room or movie")
}
func DeleteMovieByID(roomID, id string) error {
@ -37,15 +58,15 @@ func DeleteMoviesByID(roomID string, ids []string) error {
})
}
func LoadAndDeleteMovieByID(roomID, id string, 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
return movie, HandleNotFound(err, "room or movie")
}
func DeleteMoviesByRoomID(roomID string) error {
func DeleteMoviesByRoomID(roomID string, scopes ...func(*gorm.DB) *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
err := tx.Where("room_id = ?", roomID).Delete(&model.Movie{}).Error
err := tx.Where("room_id = ?", roomID).Scopes(scopes...).Delete(&model.Movie{}).Error
if err != nil {
return HandleNotFound(err, "room")
}
@ -53,6 +74,10 @@ func DeleteMoviesByRoomID(roomID string) error {
})
}
func DeleteMoviesByRoomIDAndParentID(roomID string, parentID string) error {
return DeleteMoviesByRoomID(roomID, WithParentMovieID(parentID))
}
func LoadAndDeleteMoviesByRoomID(roomID string, columns ...clause.Column) ([]*model.Movie, error) {
movies := []*model.Movie{}
err := db.Transaction(func(tx *gorm.DB) error {

@ -15,7 +15,7 @@ type dbVersion struct {
Upgrade func(*gorm.DB) error
}
const CurrentVersion = "0.0.8"
const CurrentVersion = "0.0.9"
var models = []any{
new(model.Setting),
@ -70,6 +70,9 @@ var dbVersions = map[string]dbVersion{
NextVersion: "0.0.8",
},
"0.0.8": {
NextVersion: "0.0.9",
},
"0.0.9": {
NextVersion: "",
},
}

@ -66,10 +66,7 @@ func WithRegisteredByProvider(b bool) CreateUserConfig {
func WithEmail(email string) CreateUserConfig {
return func(u *model.User) {
u.Email = sql.NullString{
String: email,
Valid: true,
}
u.Email = model.EmptyNullString(email)
}
}
@ -441,7 +438,7 @@ func UnbindEmail(uid string) error {
if err := tx.Where("id = ?", uid).First(&user).Error; err != nil {
return HandleNotFound(err, "user")
}
if user.Email.String == "" {
if user.Email == "" {
return errors.New("user has no email")
}
if user.RegisteredByEmail {

@ -1,6 +1,7 @@
package model
import (
"database/sql/driver"
"fmt"
"strings"
"time"
@ -16,7 +17,8 @@ type Movie struct {
Position uint `gorm:"not null" json:"-"`
RoomID string `gorm:"not null;index;type:char(32)" json:"-"`
CreatorID string `gorm:"index;type:char(32)" json:"creatorId"`
Base BaseMovie `gorm:"embedded;embeddedPrefix:base_" json:"base"`
MovieBase `gorm:"embedded;embeddedPrefix:base_" json:"base"`
Children []*Movie `gorm:"foreignKey:ParentID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"`
}
func (m *Movie) BeforeCreate(tx *gorm.DB) error {
@ -26,17 +28,71 @@ func (m *Movie) BeforeCreate(tx *gorm.DB) error {
return nil
}
type BaseMovie struct {
func (m *Movie) BeforeSave(tx *gorm.DB) (err error) {
if m.ParentID != "" {
mv := &Movie{}
err = tx.Where("id = ?", m.ParentID).First(mv).Error
if err != nil {
return fmt.Errorf("load parent movie failed: %w", err)
}
if !mv.IsFolder {
return fmt.Errorf("parent is not a folder")
}
if mv.IsDynamicFolder() {
return fmt.Errorf("parent is a dynamic folder, cannot add child")
}
}
return
}
type MovieBase struct {
Url string `gorm:"type:varchar(8192)" json:"url"`
MoreSource map[string]string `gorm:"serializer:fastjson;type:text" json:"moreSource"`
MoreSource map[string]string `gorm:"serializer:fastjson;type:text" json:"moreSource,omitempty"`
Name string `gorm:"not null;type:varchar(256)" json:"name"`
Live bool `json:"live"`
Proxy bool `json:"proxy"`
RtmpSource bool `json:"rtmpSource"`
Type string `json:"type"`
Headers map[string]string `gorm:"serializer:fastjson;type:text" json:"headers"`
Subtitles map[string]*Subtitle `gorm:"serializer:fastjson;type:text" json:"subtitles"`
Headers map[string]string `gorm:"serializer:fastjson;type:text" json:"headers,omitempty"`
Subtitles map[string]*Subtitle `gorm:"serializer:fastjson;type:text" json:"subtitles,omitempty"`
VendorInfo VendorInfo `gorm:"embedded;embeddedPrefix:vendor_info_" json:"vendorInfo,omitempty"`
IsFolder bool `json:"isFolder"`
ParentID EmptyNullString `gorm:"type:char(32)" json:"parentId"`
}
func (m *MovieBase) IsDynamicFolder() bool {
return m.IsFolder && m.VendorInfo.Vendor != ""
}
type EmptyNullString string
func (ns EmptyNullString) String() string {
return string(ns)
}
// Scan implements the [Scanner] interface.
func (ns *EmptyNullString) Scan(value any) error {
if value == nil {
*ns = ""
return nil
}
switch v := value.(type) {
case []byte:
*ns = EmptyNullString(v)
case string:
*ns = EmptyNullString(v)
default:
return fmt.Errorf("unsupported type: %T", v)
}
return nil
}
// Value implements the [driver.Valuer] interface.
func (ns EmptyNullString) Value() (driver.Value, error) {
if ns == "" {
return nil, nil
}
return string(ns), nil
}
type Subtitle struct {
@ -102,6 +158,28 @@ func GetAlistServerIdFromPath(path string) (serverID string, filePath string, er
return before, after, nil
}
func FormatAlistPath(serverID, filePath string) string {
return fmt.Sprintf("%s/%s", serverID, strings.Trim(filePath, "/"))
}
func (a *AlistStreamingInfo) SetServerIDAndFilePath(serverID, filePath string) {
a.Path = FormatAlistPath(serverID, filePath)
}
func (a *AlistStreamingInfo) ServerID() (string, error) {
serverID, _, err := GetAlistServerIdFromPath(a.Path)
return serverID, err
}
func (a *AlistStreamingInfo) FilePath() (string, error) {
_, filePath, err := GetAlistServerIdFromPath(a.Path)
return filePath, err
}
func (a *AlistStreamingInfo) ServerIDAndFilePath() (serverID, filePath string, err error) {
return GetAlistServerIdFromPath(a.Path)
}
func (a *AlistStreamingInfo) Validate() error {
if a.Path == "" {
return fmt.Errorf("path is empty")
@ -148,6 +226,28 @@ func GetEmbyServerIdFromPath(path string) (serverID string, filePath string, err
return "", path, fmt.Errorf("path is invalid")
}
func FormatEmbyPath(serverID, filePath string) string {
return fmt.Sprintf("%s/%s", serverID, filePath)
}
func (e *EmbyStreamingInfo) SetServerIDAndFilePath(serverID, filePath string) {
e.Path = FormatEmbyPath(serverID, filePath)
}
func (e *EmbyStreamingInfo) ServerID() (string, error) {
serverID, _, err := GetEmbyServerIdFromPath(e.Path)
return serverID, err
}
func (e *EmbyStreamingInfo) FilePath() (string, error) {
_, filePath, err := GetEmbyServerIdFromPath(e.Path)
return filePath, err
}
func (e *EmbyStreamingInfo) ServerIDAndFilePath() (serverID, filePath string, err error) {
return GetEmbyServerIdFromPath(e.Path)
}
func (e *EmbyStreamingInfo) Validate() error {
if e.Path == "" {
return fmt.Errorf("path is empty")

@ -1,7 +1,6 @@
package model
import (
"database/sql"
"fmt"
"math/rand"
"time"
@ -48,7 +47,7 @@ type User struct {
UserProviders []*UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Username string `gorm:"not null;uniqueIndex;type:varchar(32)"`
HashedPassword []byte `gorm:"not null"`
Email sql.NullString `gorm:"type:varchar(128);uniqueIndex"`
Email EmptyNullString `gorm:"type:varchar(128);uniqueIndex"`
Role Role `gorm:"not null;default:2"`
RoomMembers []*RoomMember `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Rooms []*Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`

@ -11,9 +11,13 @@ type current struct {
}
type Current struct {
MovieID string
IsLive bool
Status Status
Movie CurrentMovie
Status Status
}
type CurrentMovie struct {
ID string
IsLive bool
}
func newCurrent() *current {
@ -46,12 +50,11 @@ func (c *current) Current() Current {
return c.current
}
func (c *current) SetMovie(movieID string, isLive, play bool) {
func (c *current) SetMovie(movie CurrentMovie, play bool) {
c.lock.Lock()
defer c.lock.Unlock()
c.current.MovieID = movieID
c.current.IsLive = isLive
c.current.Movie = movie
c.current.SetSeek(0, 0)
c.current.Status.Playing = play
}
@ -80,7 +83,7 @@ func (c *current) SetSeekRate(seek, rate, timeDiff float64) *Status {
}
func (c *Current) UpdateStatus() Status {
if c.IsLive {
if c.Movie.IsLive {
c.Status.lastUpdate = time.Now()
return c.Status
}
@ -100,7 +103,7 @@ func (c *Current) setLiveStatus() Status {
}
func (c *Current) SetStatus(playing bool, seek, rate, timeDiff float64) Status {
if c.IsLive {
if c.Movie.IsLive {
return c.setLiveStatus()
}
c.Status.Playing = playing
@ -115,7 +118,7 @@ func (c *Current) SetStatus(playing bool, seek, rate, timeDiff float64) Status {
}
func (c *Current) SetSeekRate(seek, rate, timeDiff float64) Status {
if c.IsLive {
if c.Movie.IsLive {
return c.setLiveStatus()
}
if c.Status.Playing {
@ -129,7 +132,7 @@ func (c *Current) SetSeekRate(seek, rate, timeDiff float64) Status {
}
func (c *Current) SetSeek(seek, timeDiff float64) Status {
if c.IsLive {
if c.Movie.IsLive {
return c.setLiveStatus()
}
if c.Status.Playing {

@ -29,29 +29,40 @@ type Movie struct {
alistCache atomic.Pointer[cache.AlistMovieCache]
bilibiliCache atomic.Pointer[cache.BilibiliMovieCache]
embyCache atomic.Pointer[cache.EmbyMovieCache]
subPath string
}
func (m *Movie) SubPath() string {
return m.subPath
}
func (m *Movie) ExpireId() uint64 {
if m.IsFolder {
return 0
}
switch {
case m.Movie.Base.VendorInfo.Vendor == model.VendorAlist:
case m.Movie.MovieBase.VendorInfo.Vendor == model.VendorAlist:
amcd, _ := m.AlistCache().Raw()
if amcd != nil && amcd.Ali != nil {
return uint64(m.AlistCache().Last())
}
case m.Movie.Base.Live && m.Movie.Base.VendorInfo.Vendor == model.VendorBilibili:
case m.Movie.MovieBase.Live && m.Movie.MovieBase.VendorInfo.Vendor == model.VendorBilibili:
return uint64(m.BilibiliCache().Live.Last())
}
return uint64(crc32.ChecksumIEEE([]byte(m.Movie.ID)))
}
func (m *Movie) CheckExpired(expireId uint64) bool {
if m.IsFolder {
return false
}
switch {
case m.Movie.Base.VendorInfo.Vendor == model.VendorAlist:
case m.Movie.MovieBase.VendorInfo.Vendor == model.VendorAlist:
amcd, _ := m.AlistCache().Raw()
if amcd != nil && amcd.Ali != nil {
return time.Now().UnixNano()-int64(expireId) > m.AlistCache().MaxAge()
}
case m.Movie.Base.Live && m.Movie.Base.VendorInfo.Vendor == model.VendorBilibili:
case m.Movie.MovieBase.Live && m.Movie.MovieBase.VendorInfo.Vendor == model.VendorBilibili:
return time.Now().UnixNano()-int64(expireId) > m.BilibiliCache().Live.MaxAge()
}
return expireId != m.ExpireId()
@ -83,7 +94,7 @@ func (m *Movie) ClearCache() error {
func (m *Movie) AlistCache() *cache.AlistMovieCache {
c := m.alistCache.Load()
if c == nil {
c = cache.NewAlistMovieCache(m.Movie)
c = cache.NewAlistMovieCache(m.Movie, m.subPath)
if !m.alistCache.CompareAndSwap(nil, c) {
return m.AlistCache()
}
@ -105,7 +116,7 @@ func (m *Movie) BilibiliCache() *cache.BilibiliMovieCache {
func (m *Movie) EmbyCache() *cache.EmbyMovieCache {
c := m.embyCache.Load()
if c == nil {
c = cache.NewEmbyMovieCache(m.Movie)
c = cache.NewEmbyMovieCache(m.Movie, m.subPath)
if !m.embyCache.CompareAndSwap(nil, c) {
return m.EmbyCache()
}
@ -114,6 +125,9 @@ func (m *Movie) EmbyCache() *cache.EmbyMovieCache {
}
func (m *Movie) Channel() (*rtmps.Channel, error) {
if m.IsFolder {
return nil, errors.New("this is a folder")
}
err := m.initChannel()
if err != nil {
return nil, err
@ -139,10 +153,10 @@ func (m *Movie) compareAndSwapInitChannel() *rtmps.Channel {
func (m *Movie) initChannel() error {
switch {
case m.Movie.Base.Live && m.Movie.Base.RtmpSource:
case m.Movie.MovieBase.Live && m.Movie.MovieBase.RtmpSource:
m.compareAndSwapInitChannel()
case m.Movie.Base.Live && m.Movie.Base.Proxy:
u, err := url.Parse(m.Movie.Base.Url)
case m.Movie.MovieBase.Live && m.Movie.MovieBase.Proxy:
u, err := url.Parse(m.Movie.MovieBase.Url)
if err != nil {
return err
}
@ -159,7 +173,7 @@ func (m *Movie) initChannel() error {
return
}
cli := core.NewConnClient()
if err = cli.Start(m.Movie.Base.Url, av.PLAY); err != nil {
if err = cli.Start(m.Movie.MovieBase.Url, av.PLAY); err != nil {
cli.Close()
time.Sleep(time.Second)
continue
@ -182,11 +196,11 @@ func (m *Movie) initChannel() error {
return
}
r := resty.New().R()
for k, v := range m.Movie.Base.Headers {
for k, v := range m.Movie.MovieBase.Headers {
r.SetHeader(k, v)
}
// r.SetHeader("User-Agent", UserAgent)
resp, err := r.Get(m.Movie.Base.Url)
resp, err := r.Get(m.Movie.MovieBase.Url)
if err != nil {
time.Sleep(time.Second)
continue
@ -207,13 +221,16 @@ func (m *Movie) initChannel() error {
}
func (movie *Movie) Validate() error {
m := movie.Movie.Base
m := movie.Movie.MovieBase
if m.VendorInfo.Vendor != "" {
err := movie.validateVendorMovie()
if err != nil {
return err
}
}
if movie.IsFolder {
return nil
}
switch {
case m.RtmpSource && m.Proxy:
return errors.New("rtmp source and proxy can't be true at the same time")
@ -274,15 +291,18 @@ func (movie *Movie) Validate() error {
}
func (movie *Movie) validateVendorMovie() error {
switch movie.Movie.Base.VendorInfo.Vendor {
switch movie.Movie.MovieBase.VendorInfo.Vendor {
case model.VendorBilibili:
return movie.Movie.Base.VendorInfo.Bilibili.Validate()
if movie.IsFolder {
return errors.New("bilibili folder not support")
}
return movie.Movie.MovieBase.VendorInfo.Bilibili.Validate()
case model.VendorAlist:
return movie.Movie.Base.VendorInfo.Alist.Validate()
return movie.Movie.MovieBase.VendorInfo.Alist.Validate()
case model.VendorEmby:
return movie.Movie.Base.VendorInfo.Emby.Validate()
return movie.Movie.MovieBase.VendorInfo.Emby.Validate()
default:
return fmt.Errorf("vendor not implement validate")
@ -290,6 +310,9 @@ func (movie *Movie) validateVendorMovie() error {
}
func (m *Movie) Terminate() error {
if m.IsFolder {
return nil
}
c := m.channel.Swap(nil)
if c != nil {
err := c.Close()
@ -300,8 +323,14 @@ func (m *Movie) Terminate() error {
return nil
}
func (m *Movie) Update(movie *model.BaseMovie) error {
m.Movie.Base = *movie
m.ClearCache()
return m.Terminate()
func (m *Movie) Close() error {
err := m.Terminate()
if err != nil {
return err
}
err = m.ClearCache()
if err != nil {
return err
}
return nil
}

@ -2,44 +2,21 @@ package op
import (
"errors"
"sync"
"time"
"github.com/synctv-org/synctv/internal/db"
"github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/utils"
"github.com/zijiren233/gencontainer/dllist"
"github.com/zijiren233/gencontainer/rwmap"
rtmps "github.com/zijiren233/livelib/server"
"gorm.io/gorm"
)
type movies struct {
roomID string
lock sync.RWMutex
list dllist.Dllist[*Movie]
once sync.Once
}
func (m *movies) init() {
m.once.Do(func() {
for _, m2 := range db.GetAllMoviesByRoomID(m.roomID) {
m.list.PushBack(&Movie{
Movie: m2,
})
}
})
}
func (m *movies) Len() int {
m.init()
m.lock.RLock()
defer m.lock.RUnlock()
return m.list.Len()
cache rwmap.RWMap[string, *Movie]
}
func (m *movies) AddMovie(mo *model.Movie) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
mo.Position = uint(time.Now().UnixMilli())
movie := &Movie{
Movie: mo,
@ -55,14 +32,14 @@ func (m *movies) AddMovie(mo *model.Movie) error {
return err
}
m.list.PushBack(movie)
old, ok := m.cache.Swap(mo.ID, movie)
if ok {
_ = old.Close()
}
return nil
}
func (m *movies) AddMovies(mos []*model.Movie) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
inited := make([]*Movie, 0, len(mos))
for _, mo := range mos {
mo.Position = uint(time.Now().UnixMilli())
@ -84,7 +61,10 @@ func (m *movies) AddMovies(mos []*model.Movie) error {
}
for _, mo := range inited {
m.list.PushBack(mo)
old, ok := m.cache.Swap(mo.Movie.ID, mo)
if ok {
_ = old.Close()
}
}
return nil
@ -101,170 +81,132 @@ func (m *movies) GetChannel(id string) (*rtmps.Channel, error) {
return movie.Channel()
}
func (m *movies) Update(movieId string, movie *model.BaseMovie) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.Movie.ID == movieId {
err := e.Value.Update(movie)
if err != nil {
return err
}
return db.SaveMovie(e.Value.Movie)
}
func (m *movies) Update(movieId string, movie *model.MovieBase) error {
mv, err := db.GetMovieByID(m.roomID, movieId)
if err != nil {
return err
}
return nil
}
func (m *movies) Clear() error {
m.lock.Lock()
defer m.lock.Unlock()
err := db.DeleteMoviesByRoomID(m.roomID)
mv.MovieBase = *movie
err = db.SaveMovie(mv)
if err != nil {
return err
}
for e := m.list.Front(); e != nil; e = e.Next() {
_ = e.Value.Terminate()
mm, ok := m.cache.LoadOrStore(mv.ID, &Movie{Movie: mv})
if ok {
_ = mm.Close()
}
m.list.Clear()
return nil
}
func (m *movies) Clear() error {
return m.DeleteMovieByParentID("")
}
func (m *movies) Close() error {
m.lock.Lock()
defer m.lock.Unlock()
for e := m.list.Front(); e != nil; e = e.Next() {
_ = e.Value.Terminate()
m.DeleteMovieAndChiledCache("")
return nil
}
func (m *movies) DeleteMovieByParentID(parentID string) error {
err := db.DeleteMoviesByRoomIDAndParentID(m.roomID, parentID)
if err != nil {
return err
}
m.list.Clear()
m.DeleteMovieAndChiledCache("")
return nil
}
func (m *movies) DeleteMovieByID(id string) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
err := db.DeleteMovieByID(m.roomID, id)
if err != nil {
return err
}
m.DeleteMovieAndChiledCache(id)
return nil
}
func (m *movies) DeleteMovieAndChiledCache(id ...string) {
idm := make(map[model.EmptyNullString]struct{}, len(id))
for _, id := range id {
idm[model.EmptyNullString(id)] = struct{}{}
}
m.deleteMovieAndChiledCache(idm)
}
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.Movie.ID == id {
_ = m.list.Remove(e).Terminate()
return nil
func (m *movies) deleteMovieAndChiledCache(ids map[model.EmptyNullString]struct{}) {
next := make(map[model.EmptyNullString]struct{})
m.cache.Range(func(key string, value *Movie) bool {
if _, ok := ids[value.ParentID]; ok {
if value.IsFolder {
next[model.EmptyNullString(value.ID)] = struct{}{}
} else {
m.cache.Delete(key)
}
value.Close()
}
return true
})
if len(next) > 0 {
m.deleteMovieAndChiledCache(next)
}
return errors.New("movie not found")
}
func (m *movies) DeleteMoviesByID(ids []string) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
err := db.DeleteMoviesByID(m.roomID, ids)
if err != nil {
return err
}
for _, id := range ids {
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.Movie.ID == id {
_ = m.list.Remove(e).Terminate()
break
}
}
}
m.DeleteMovieAndChiledCache(ids...)
return nil
}
func (m *movies) GetMovieByID(id string) (*Movie, error) {
m.lock.RLock()
defer m.lock.RUnlock()
return m.getMovieByID(id)
}
func (m *movies) getMovieByID(id string) (*Movie, error) {
if id == "" {
return nil, errors.New("movie id is nil")
}
m.init()
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.Movie.ID == id {
return e.Value, nil
}
mm, ok := m.cache.Load(id)
if ok {
return mm, nil
}
return nil, errors.New("movie not found")
}
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.Movie.ID == id {
return e, nil
}
mv, err := db.GetMovieByID(m.roomID, id)
if err != nil {
return nil, err
}
return nil, errors.New("movie not found")
mo := &Movie{Movie: mv}
mm, _ = m.cache.LoadOrStore(mv.ID, mo)
return mm, nil
}
func (m *movies) SwapMoviePositions(id1, id2 string) error {
m.init()
m.lock.Lock()
defer m.lock.Unlock()
return db.SwapMoviePositions(m.roomID, id1, id2)
}
err := db.SwapMoviePositions(m.roomID, id1, id2)
if err != nil {
return err
func (m *movies) GetMoviesWithPage(page, pageSize int, parentID string) ([]*model.Movie, int64, error) {
scopes := []func(*gorm.DB) *gorm.DB{
db.WithParentMovieID(parentID),
}
movie1, err := m.getMovieElementByID(id1)
count, err := db.GetMoviesCountByRoomID(m.roomID, append(scopes, db.Paginate(page, pageSize))...)
if err != nil {
return err
return nil, 0, err
}
movie2, err := m.getMovieElementByID(id2)
movies, err := db.GetMoviesByRoomID(m.roomID, scopes...)
if err != nil {
return err
return nil, 0, err
}
movie1.Value.Movie.Position, movie2.Value.Movie.Position = movie2.Value.Movie.Position, movie1.Value.Movie.Position
m.list.Swap(movie1, movie2)
return nil
return movies, count, nil
}
func (m *movies) GetMoviesWithPage(page, pageSize int, creator string) ([]*Movie, int) {
m.init()
m.lock.RLock()
defer m.lock.RUnlock()
var total int
if creator != "" {
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.Movie.CreatorID == creator {
total++
}
}
} else {
total = m.list.Len()
// IsParentOf check if parentID is the parent of id
func (m *movies) IsParentOf(parentID, id string) (bool, error) {
mv, err := m.GetMovieByID(id)
if err != nil {
return false, err
}
start, end := utils.GetPageItemsRange(total, page, pageSize)
ms := make([]*Movie, 0, end-start)
i := 0
for e := m.list.Front(); e != nil; e = e.Next() {
if creator != "" && e.Value.Movie.CreatorID != creator {
continue
}
if i >= start && i < end {
ms = append(ms, e.Value)
} else if i >= end {
return ms, total
}
i++
if mv.ParentID == "" {
return false, nil
}
if mv.ParentID == model.EmptyNullString(parentID) {
return true, nil
}
return ms, total
return m.IsParentOf(string(mv.ParentID), id)
}

@ -23,7 +23,7 @@ type Room struct {
current *current
initOnce utils.Once
hub *Hub
movies movies
movies *movies
members rwmap.RWMap[string, *model.RoomMember]
}
@ -80,9 +80,19 @@ func (r *Room) CheckVersion(version uint32) bool {
return atomic.LoadUint32(&r.version) == version
}
func (r *Room) UpdateMovie(movieId string, movie *model.BaseMovie) error {
if r.current.current.MovieID == movieId {
return errors.New("cannot update current movie")
func (r *Room) UpdateMovie(movieId string, movie *model.MovieBase) error {
cid := r.current.current.Movie.ID
if cid != "" {
if cid == movieId {
return errors.New("cannot update current movie")
}
ok, err := r.IsParentOf(cid, movieId)
if err != nil {
return fmt.Errorf("check parent failed: %w", err)
}
if ok {
return errors.New("cannot update current movie's parent")
}
}
return r.movies.Update(movieId, movie)
}
@ -329,31 +339,68 @@ func (r *Room) SetPassword(password string) error {
return db.SetRoomHashedPassword(r.ID, hashedPassword)
}
func (r *Room) GetMoviesCount() int {
return r.movies.Len()
func (r *Room) IsParentOf(movieID, parentID string) (bool, error) {
if parentID == "" {
return true, nil
}
return r.movies.IsParentOf(movieID, parentID)
}
func (r *Room) DeleteMovieByID(id string) error {
if r.current.current.MovieID == id {
return errors.New("cannot delete current movie")
if id == "" {
return errors.New("movie id is nil")
}
cid := r.current.current.Movie.ID
if cid != "" {
if cid == id {
return errors.New("cannot delete current movie")
}
ok, err := r.IsParentOf(cid, id)
if err != nil {
return fmt.Errorf("check parent failed: %w", err)
}
if ok {
return errors.New("cannot delete current movie's parent")
}
}
return r.movies.DeleteMovieByID(id)
}
func (r *Room) DeleteMoviesByID(ids []string) error {
if r.current.current.MovieID != "" {
cid := r.current.current.Movie.ID
if cid != "" {
for _, id := range ids {
if id == r.current.current.MovieID {
if id == cid {
return errors.New("cannot delete current movie")
}
ok, err := r.IsParentOf(cid, id)
if err != nil {
return fmt.Errorf("check parent failed: %w", err)
}
if ok {
return errors.New("cannot delete current movie's parent")
}
}
}
return r.movies.DeleteMoviesByID(ids)
}
func (r *Room) ClearMovies() error {
_ = r.SetCurrentMovie("", false)
return r.movies.Clear()
return r.ClearMoviesByParentID("")
}
func (r *Room) ClearMoviesByParentID(parentID string) error {
cid := r.current.current.Movie.ID
if cid != "" {
ok, err := r.IsParentOf(cid, parentID)
if err != nil {
return fmt.Errorf("check parent failed: %w", err)
}
if ok {
return errors.New("cannot delete current movie's parent")
}
}
return r.movies.DeleteMovieByParentID(parentID)
}
func (r *Room) GetMovieByID(id string) (*Movie, error) {
@ -365,57 +412,62 @@ func (r *Room) Current() *Current {
return &c
}
func (r *Room) CurrentMovieID() string {
return r.current.current.MovieID
func (r *Room) CurrentMovie() CurrentMovie {
return r.current.current.Movie
}
var ErrNoCurrentMovie = errors.New("no current movie")
func (r *Room) CurrentMovie() (*Movie, error) {
if r.current.current.MovieID == "" {
func (r *Room) LoadCurrentMovie() (*Movie, error) {
id := r.current.current.Movie.ID
if id == "" {
return nil, ErrNoCurrentMovie
}
return r.GetMovieByID(r.current.current.MovieID)
return r.GetMovieByID(id)
}
func (r *Room) CheckCurrentExpired(expireId uint64) (bool, error) {
m, err := r.CurrentMovie()
m, err := r.LoadCurrentMovie()
if err != nil {
return false, err
}
return m.CheckExpired(expireId), nil
}
func (r *Room) SetCurrentMovie(movieID string, play bool) error {
currentMovie, err := r.CurrentMovie()
func (r *Room) SetCurrentMovie(movieID string, subPath string, play bool) error {
currentMovie, err := r.LoadCurrentMovie()
if err != nil {
if err != ErrNoCurrentMovie {
return err
}
} else {
err = currentMovie.ClearCache()
if err != nil {
return fmt.Errorf("clear cache failed: %w", err)
}
_ = currentMovie.ClearCache()
}
if movieID == "" {
r.current.SetMovie("", false, play)
r.current.SetMovie(CurrentMovie{}, false)
return nil
}
m, err := r.GetMovieByID(movieID)
if err != nil {
return err
}
r.current.SetMovie(m.ID, m.Base.Live, play)
return nil
if m.IsFolder && !m.IsDynamicFolder() {
return errors.New("cannot set static folder as current movie")
}
m.subPath = subPath
r.current.SetMovie(CurrentMovie{
ID: m.ID,
IsLive: m.Live,
}, play)
return m.ClearCache()
}
func (r *Room) SwapMoviePositions(id1, id2 string) error {
return r.movies.SwapMoviePositions(id1, id2)
}
func (r *Room) GetMoviesWithPage(page, pageSize int, creator string) ([]*Movie, int) {
return r.movies.GetMoviesWithPage(page, pageSize, creator)
func (r *Room) GetMoviesWithPage(page, pageSize int, parentID string) ([]*model.Movie, int64, error) {
return r.movies.GetMoviesWithPage(page, pageSize, parentID)
}
func (r *Room) NewClient(user *User, conn *websocket.Conn) (*Client, error) {

@ -67,9 +67,7 @@ func LoadOrInitRoom(room *model.Room) (*RoomEntry, error) {
Room: *room,
version: crc32.ChecksumIEEE(room.HashedPassword),
current: newCurrent(),
movies: movies{
roomID: room.ID,
},
movies: &movies{roomID: room.ID},
}, time.Duration(settings.RoomTTL.Get())*time.Hour)
return i, nil
}

@ -1,7 +1,6 @@
package op
import (
"database/sql"
"errors"
"hash/crc32"
"sync/atomic"
@ -107,7 +106,7 @@ func (u *User) CreateRoom(name, password string, conf ...db.CreateRoomConfig) (*
return CreateRoom(name, password, maxCount, append(conf, db.WithCreator(&u.User))...)
}
func (u *User) NewMovie(movie *model.BaseMovie) (*model.Movie, error) {
func (u *User) NewMovie(movie *model.MovieBase) (*model.Movie, error) {
if movie == nil {
return nil, errors.New("movie is nil")
}
@ -122,12 +121,12 @@ func (u *User) NewMovie(movie *model.BaseMovie) (*model.Movie, error) {
}
}
return &model.Movie{
Base: *movie,
MovieBase: *movie,
CreatorID: u.ID,
}, nil
}
func (u *User) AddRoomMovie(room *Room, movie *model.BaseMovie) (*model.Movie, error) {
func (u *User) AddRoomMovie(room *Room, movie *model.MovieBase) (*model.Movie, error) {
if !u.HasRoomPermission(room, model.PermissionAddMovie) {
return nil, model.ErrNoPermission
}
@ -148,7 +147,7 @@ func (u *User) AddRoomMovie(room *Room, movie *model.BaseMovie) (*model.Movie, e
})
}
func (u *User) NewMovies(movies []*model.BaseMovie) ([]*model.Movie, error) {
func (u *User) NewMovies(movies []*model.MovieBase) ([]*model.Movie, error) {
var ms = make([]*model.Movie, len(movies))
for i, m := range movies {
movie, err := u.NewMovie(m)
@ -160,7 +159,7 @@ func (u *User) NewMovies(movies []*model.BaseMovie) ([]*model.Movie, error) {
return ms, nil
}
func (u *User) AddRoomMovies(room *Room, movies []*model.BaseMovie) ([]*model.Movie, error) {
func (u *User) AddRoomMovies(room *Room, movies []*model.MovieBase) ([]*model.Movie, error) {
if !u.HasRoomPermission(room, model.PermissionAddMovie) {
return nil, model.ErrNoPermission
}
@ -308,7 +307,7 @@ func (u *User) SetUsername(username string) error {
return nil
}
func (u *User) UpdateRoomMovie(room *Room, movieID string, movie *model.BaseMovie) error {
func (u *User) UpdateRoomMovie(room *Room, movieID string, movie *model.MovieBase) error {
if !u.HasRoomPermission(room, model.PermissionEditMovie) {
return model.ErrNoPermission
}
@ -389,6 +388,23 @@ func (u *User) ClearRoomMovies(room *Room) error {
})
}
func (u *User) ClearRoomMoviesByParentID(room *Room, parentID string) error {
if !u.HasRoomPermission(room, model.PermissionDeleteMovie) {
return model.ErrNoPermission
}
err := room.ClearMoviesByParentID(parentID)
if err != nil {
return err
}
return room.Broadcast(&pb.ElementMessage{
Type: pb.ElementMessageType_MOVIES_CHANGED,
MoviesChanged: &pb.Sender{
Username: u.Username,
Userid: u.ID,
},
})
}
func (u *User) SwapRoomMoviePositions(room *Room, id1, id2 string) error {
if !u.HasRoomPermission(room, model.PermissionEditMovie) {
return model.ErrNoPermission
@ -406,11 +422,11 @@ func (u *User) SwapRoomMoviePositions(room *Room, id1, id2 string) error {
})
}
func (u *User) SetRoomCurrentMovie(room *Room, movieID string, play bool) error {
func (u *User) SetRoomCurrentMovie(room *Room, movieID string, subPath string, play bool) error {
if !u.HasRoomPermission(room, model.PermissionSetCurrentMovie) {
return model.ErrNoPermission
}
err := room.SetCurrentMovie(movieID, play)
err := room.SetCurrentMovie(movieID, subPath, play)
if err != nil {
return err
}
@ -444,10 +460,7 @@ func (u *User) BindEmail(e string) error {
if err != nil {
return err
}
u.Email = sql.NullString{
String: e,
Valid: true,
}
u.Email = model.EmptyNullString(e)
return nil
}
@ -456,40 +469,40 @@ func (u *User) UnbindEmail() error {
if err != nil {
return err
}
u.Email = sql.NullString{}
u.Email = ""
return nil
}
var ErrEmailUnbound = errors.New("email unbound")
func (u *User) SendTestEmail() error {
if u.Email.String == "" {
if u.Email == "" {
return ErrEmailUnbound
}
return email.SendTestEmail(u.Username, u.Email.String)
return email.SendTestEmail(u.Username, u.Email.String())
}
func (u *User) SendRetrievePasswordCaptchaEmail(host string) error {
if u.Email.String == "" {
if u.Email == "" {
return ErrEmailUnbound
}
return email.SendRetrievePasswordCaptchaEmail(u.ID, u.Email.String, host)
return email.SendRetrievePasswordCaptchaEmail(u.ID, u.Email.String(), host)
}
func (u *User) VerifyRetrievePasswordCaptchaEmail(e, captcha string) (bool, error) {
if u.Email.String != e {
if u.Email.String() != e {
return false, errors.New("email has changed, please resend the captcha email")
}
return email.VerifyRetrievePasswordCaptchaEmail(u.ID, e, captcha)
}
func (u *User) GetRoomMoviesWithPage(room *Room, page, pageSize int) ([]*Movie, int) {
if u.HasRoomPermission(room, model.PermissionGetMovieList) {
return room.GetMoviesWithPage(page, pageSize, "")
func (u *User) GetRoomMoviesWithPage(room *Room, page, pageSize int, parentID string) ([]*model.Movie, int64, error) {
if !u.HasRoomPermission(room, model.PermissionGetMovieList) {
return nil, 0, model.ErrNoPermission
}
return room.GetMoviesWithPage(page, pageSize, u.ID)
return room.GetMoviesWithPage(page, pageSize, parentID)
}
func (u *User) SetRoomCurrentSeekRate(room *Room, seek, rate, timeDiff float64) (*Status, error) {

@ -12,6 +12,7 @@ import (
"math/rand"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
@ -21,12 +22,16 @@ import (
"github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/cache"
"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"
"github.com/synctv-org/synctv/internal/settings"
"github.com/synctv-org/synctv/internal/vendor"
"github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
"github.com/synctv-org/vendors/api/alist"
"github.com/synctv-org/vendors/api/emby"
uhc "github.com/zijiren233/go-uhc"
"github.com/zijiren233/livelib/protocol/hls"
"github.com/zijiren233/livelib/protocol/httpflv"
@ -87,59 +92,65 @@ func GetPageItems[T any](ctx *gin.Context, items []T) ([]T, error) {
func genMovieInfo(
ctx context.Context,
user *op.User,
room *op.Room,
opMovie *op.Movie,
userAgent string,
userAgent,
userToken string,
) (*model.MovieResp, error) {
) (*model.Movie, error) {
if opMovie == nil || opMovie.ID == "" {
return &model.MovieResp{}, nil
return &model.Movie{}, nil
}
if opMovie.IsFolder {
if !opMovie.IsDynamicFolder() {
return nil, errors.New("movie is static folder, can't get movie info")
}
}
var movie = *opMovie.Movie
if movie.Base.VendorInfo.Vendor != "" {
if movie.MovieBase.VendorInfo.Vendor != "" {
vendorMovie, err := genVendorMovie(ctx, user, opMovie, userAgent, userToken)
if err != nil {
return nil, err
}
movie = *vendorMovie
} else if movie.Base.RtmpSource || movie.Base.Live && movie.Base.Proxy {
switch movie.Base.Type {
} else if movie.MovieBase.RtmpSource || movie.MovieBase.Live && movie.MovieBase.Proxy {
switch movie.MovieBase.Type {
case "m3u8":
movie.Base.Url = fmt.Sprintf("/api/movie/live/hls/list/%s.m3u8?token=%s", movie.ID, userToken)
movie.MovieBase.Url = fmt.Sprintf("/api/movie/live/hls/list/%s.m3u8?token=%s", movie.ID, userToken)
case "flv":
movie.Base.Url = fmt.Sprintf("/api/movie/live/flv/%s.flv?token=%s", movie.ID, userToken)
movie.MovieBase.Url = fmt.Sprintf("/api/movie/live/flv/%s.flv?token=%s", movie.ID, userToken)
default:
return nil, errors.New("not support live movie type")
}
movie.Base.Headers = nil
} else if movie.Base.Proxy {
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Headers = nil
movie.MovieBase.Headers = nil
} else if movie.MovieBase.Proxy {
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Headers = nil
}
if movie.Base.Type == "" && movie.Base.Url != "" {
movie.Base.Type = utils.GetUrlExtension(movie.Base.Url)
if movie.MovieBase.Type == "" && movie.MovieBase.Url != "" {
movie.MovieBase.Type = utils.GetUrlExtension(movie.MovieBase.Url)
}
resp := &model.MovieResp{
resp := &model.Movie{
Id: movie.ID,
CreatedAt: movie.CreatedAt.UnixMilli(),
Base: movie.Base,
Base: movie.MovieBase,
Creator: op.GetUserName(movie.CreatorID),
CreatorId: movie.CreatorID,
SubPath: opMovie.SubPath(),
}
return resp, nil
}
func genCurrentRespWithCurrent(ctx context.Context, user *op.User, room *op.Room, current *op.Current, userAgent string, userToken string) (*model.CurrentMovieResp, error) {
if current.MovieID == "" {
func genCurrentRespWithCurrent(ctx context.Context, user *op.User, room *op.Room, userAgent string, userToken string) (*model.CurrentMovieResp, error) {
current := room.Current()
if current.Movie.ID == "" {
return &model.CurrentMovieResp{
Movie: &model.MovieResp{},
Movie: &model.Movie{},
}, nil
}
opMovie, err := room.GetMovieByID(current.MovieID)
opMovie, err := room.GetMovieByID(current.Movie.ID)
if err != nil {
return nil, fmt.Errorf("get current movie error: %w", err)
}
mr, err := genMovieInfo(ctx, user, room, opMovie, userAgent, userToken)
mr, err := genMovieInfo(ctx, user, opMovie, userAgent, userToken)
if err != nil {
return nil, fmt.Errorf("gen current movie info error: %w", err)
}
@ -156,7 +167,7 @@ func CurrentMovie(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.UserEntry).Value()
log := ctx.MustGet("log").(*logrus.Entry)
currentResp, err := genCurrentRespWithCurrent(ctx, user, room, room.Current(), ctx.GetHeader("User-Agent"), ctx.MustGet("token").(string))
currentResp, err := genCurrentRespWithCurrent(ctx, user, room, ctx.GetHeader("User-Agent"), ctx.MustGet("token").(string))
if err != nil {
log.Errorf("gen current resp error: %v", err)
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
@ -171,6 +182,17 @@ func Movies(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.UserEntry).Value()
log := ctx.MustGet("log").(*logrus.Entry)
if !user.HasRoomPermission(room, dbModel.PermissionGetMovieList) {
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(dbModel.ErrNoPermission))
return
}
id := ctx.Query("id")
if len(id) != 0 && len(id) != 32 {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id length must be 0 or 32"))
return
}
page, max, err := utils.GetPageAndMax(ctx)
if err != nil {
log.Errorf("get page and max error: %v", err)
@ -178,26 +200,223 @@ func Movies(ctx *gin.Context) {
return
}
m, total := user.GetRoomMoviesWithPage(room, int(page), int(max))
if id != "" {
mv, err := room.GetMovieByID(id)
if err != nil {
log.Errorf("get room movie by id error: %v", err)
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if !mv.IsFolder {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("parent id is not folder"))
return
}
if mv.IsDynamicFolder() {
resp, err := listVendorDynamicMovie(ctx, user, room, mv.Movie, ctx.Query("subPath"), page, max)
if err != nil {
log.Errorf("vendor dynamic movie list error: %v", err)
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
return
}
}
m, total, err := user.GetRoomMoviesWithPage(room, page, max, id)
if err != nil {
log.Errorf("get room movies with page error: %v", err)
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
paths, err := getParentMoviePath(room, id)
if err != nil {
log.Errorf("get parent movie path error: %v", err)
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
resp := &model.MoviesResp{
Total: total,
Movies: make([]*model.Movie, len(m)),
Paths: paths,
}
mresp := make([]*model.MovieResp, len(m))
for i, v := range m {
mresp[i] = &model.MovieResp{
Id: v.Movie.ID,
Base: v.Movie.Base,
Creator: op.GetUserName(v.Movie.CreatorID),
resp.Movies[i] = &model.Movie{
Id: v.ID,
CreatedAt: v.CreatedAt.UnixMilli(),
Base: v.MovieBase,
Creator: op.GetUserName(v.CreatorID),
CreatorId: v.CreatorID,
}
// hide url and headers when proxy
if user.ID != v.Movie.CreatorID && v.Movie.Base.Proxy {
mresp[i].Base.Url = ""
mresp[i].Base.Headers = nil
if user.ID != v.CreatorID && v.MovieBase.Proxy {
resp.Movies[i].Base.Url = ""
resp.Movies[i].Base.Headers = nil
}
}
ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{
"total": total,
"movies": mresp,
}))
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
}
func getParentMoviePath(room *op.Room, id string) ([]*model.MoviePath, error) {
paths := []*model.MoviePath{
{
Name: "Home",
ID: "",
},
}
if id == "" {
return paths, nil
}
for id != "" {
p, err := room.GetMovieByID(id)
if err != nil {
return nil, fmt.Errorf("get movie by id error: %w", err)
}
paths = append(paths, &model.MoviePath{
Name: p.MovieBase.Name,
ID: p.ID,
})
id = p.ParentID.String()
}
return paths, nil
}
func listVendorDynamicMovie(ctx context.Context, reqUser *op.User, room *op.Room, movie *dbModel.Movie, subPath string, page, max int) (*model.MoviesResp, error) {
if reqUser.ID != movie.CreatorID {
return nil, fmt.Errorf("list vendor dynamic folder error: %w", dbModel.ErrNoPermission)
}
// creatorE, err := op.LoadOrInitUserByID(movie.CreatorID)
// if err != nil {
// return nil, err
// }
user := reqUser
paths, err := getParentMoviePath(room, movie.ID)
if err != nil {
return nil, fmt.Errorf("get parent movie path error: %w", err)
}
resp := &model.MoviesResp{
Paths: paths,
Dynamic: true,
}
switch movie.MovieBase.VendorInfo.Vendor {
case dbModel.VendorAlist:
serverID, truePath, err := movie.VendorInfo.Alist.ServerIDAndFilePath()
if err != nil {
return nil, fmt.Errorf("load alist server id error: %w", err)
}
newPath := path.Join(truePath, subPath)
// check new path is in parent path
if !strings.HasPrefix(newPath, truePath) {
return nil, fmt.Errorf("sub path is not in parent path")
}
truePath = newPath
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
return nil, errors.New("alist server not found")
}
return nil, err
}
var cli = vendor.LoadAlistClient(movie.VendorInfo.Backend)
data, err := cli.FsList(ctx, &alist.FsListReq{
Token: aucd.Token,
Password: movie.VendorInfo.Alist.Password,
Path: truePath,
Host: aucd.Host,
Refresh: false,
Page: uint64(page),
PerPage: uint64(max),
})
if err != nil {
return nil, err
}
resp.Total = int64(data.Total)
resp.Movies = make([]*model.Movie, len(data.Content))
for i, flr := range data.Content {
resp.Movies[i] = &model.Movie{
Id: movie.ID,
CreatedAt: movie.CreatedAt.UnixMilli(),
Creator: op.GetUserName(movie.CreatorID),
CreatorId: movie.CreatorID,
SubPath: fmt.Sprintf("/%s", strings.Trim(fmt.Sprintf("%s/%s", subPath, flr.Name), "/")),
Base: dbModel.MovieBase{
Name: flr.Name,
IsFolder: flr.IsDir,
ParentID: dbModel.EmptyNullString(movie.ID),
VendorInfo: dbModel.VendorInfo{
Vendor: dbModel.VendorAlist,
Backend: movie.VendorInfo.Backend,
Alist: &dbModel.AlistStreamingInfo{
Path: dbModel.FormatAlistPath(serverID, fmt.Sprintf("/%s", strings.Trim(fmt.Sprintf("%s/%s", truePath, flr.Name), "/"))),
},
},
},
}
}
resp.Paths = model.GenDefaultSubPaths(subPath, true, resp.Paths...)
case dbModel.VendorEmby:
serverID, truePath, err := movie.VendorInfo.Emby.ServerIDAndFilePath()
if err != nil {
return nil, fmt.Errorf("load emby server id error: %w", err)
}
if subPath != "" {
truePath = subPath
}
aucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
return nil, errors.New("emby server not found")
}
return nil, err
}
var cli = vendor.LoadEmbyClient(movie.VendorInfo.Backend)
data, err := cli.FsList(ctx, &emby.FsListReq{
Host: aucd.Host,
Path: truePath,
Token: aucd.ApiKey,
UserId: aucd.UserID,
Limit: uint64(max),
StartIndex: uint64((page - 1) * max),
})
if err != nil {
return nil, fmt.Errorf("emby fs list error: %w", err)
}
resp.Total = int64(data.Total)
resp.Movies = make([]*model.Movie, len(data.Items))
for i, flr := range data.Items {
resp.Movies[i] = &model.Movie{
Id: movie.ID,
CreatedAt: movie.CreatedAt.UnixMilli(),
Creator: op.GetUserName(movie.CreatorID),
CreatorId: movie.CreatorID,
SubPath: flr.Id,
Base: dbModel.MovieBase{
Name: flr.Name,
IsFolder: flr.IsFolder,
ParentID: dbModel.EmptyNullString(movie.ID),
VendorInfo: dbModel.VendorInfo{
Vendor: dbModel.VendorEmby,
Backend: movie.VendorInfo.Backend,
Emby: &dbModel.EmbyStreamingInfo{
Path: dbModel.FormatEmbyPath(serverID, flr.Id),
},
},
},
}
}
default:
return nil, fmt.Errorf("%v vendor not implement list dynamic movie", movie.MovieBase.VendorInfo.Vendor)
}
return resp, nil
}
func PushMovie(ctx *gin.Context) {
@ -212,7 +431,7 @@ func PushMovie(ctx *gin.Context) {
return
}
m, err := user.AddRoomMovie(room, (*dbModel.BaseMovie)(&req))
m, err := user.AddRoomMovie(room, (*dbModel.MovieBase)(&req))
if err != nil {
log.Errorf("push movie error: %v", err)
if errors.Is(err, dbModel.ErrNoPermission) {
@ -242,11 +461,10 @@ func PushMovies(ctx *gin.Context) {
return
}
var ms []*dbModel.BaseMovie = make([]*dbModel.BaseMovie, len(req))
var ms []*dbModel.MovieBase = make([]*dbModel.MovieBase, len(req))
for i, v := range req {
m := (*dbModel.BaseMovie)(v)
ms[i] = m
ms[i] = (*dbModel.MovieBase)(v)
}
m, err := user.AddRoomMovies(room, ms)
@ -304,7 +522,7 @@ func NewPublishKey(ctx *gin.Context) {
return
}
if !movie.Movie.Base.RtmpSource {
if !movie.Movie.MovieBase.RtmpSource {
log.Errorf("new publish key error: %v", "only rtmp source movie can get publish key")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("only live movie can get publish key"))
return
@ -344,7 +562,7 @@ func EditMovie(ctx *gin.Context) {
return
}
if err := user.UpdateRoomMovie(room, req.Id, (*dbModel.BaseMovie)(&req.PushMovieReq)); err != nil {
if err := user.UpdateRoomMovie(room, req.Id, (*dbModel.MovieBase)(&req.PushMovieReq)); err != nil {
log.Errorf("edit movie error: %v", err)
if errors.Is(err, dbModel.ErrNoPermission) {
ctx.AbortWithStatusJSON(
@ -397,7 +615,13 @@ func ClearMovies(ctx *gin.Context) {
room := ctx.MustGet("room").(*op.RoomEntry).Value()
user := ctx.MustGet("user").(*op.UserEntry).Value()
if err := user.ClearRoomMovies(room); err != nil {
var req model.ClearMoviesReq
if err := model.Decode(ctx, &req); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if err := user.ClearRoomMoviesByParentID(room, req.ParentId); err != nil {
if errors.Is(err, dbModel.ErrNoPermission) {
ctx.AbortWithStatusJSON(
http.StatusForbidden,
@ -437,14 +661,14 @@ func ChangeCurrentMovie(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.UserEntry).Value()
log := ctx.MustGet("log").(*logrus.Entry)
req := model.IdCanEmptyReq{}
req := model.SetRoomCurrentMovieReq{}
err := model.Decode(ctx, &req)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
err = user.SetRoomCurrentMovie(room, req.Id, req.Id != "")
err = user.SetRoomCurrentMovie(room, req.Id, req.SubPath, true)
if err != nil {
log.Errorf("change current movie error: %v", err)
if errors.Is(err, dbModel.ErrNoPermission) {
@ -492,22 +716,22 @@ func ProxyMovie(ctx *gin.Context) {
return
}
if m.Movie.Base.VendorInfo.Vendor != "" {
if m.Movie.MovieBase.VendorInfo.Vendor != "" {
proxyVendorMovie(ctx, m)
return
}
if !m.Movie.Base.Proxy || m.Movie.Base.Live || m.Movie.Base.RtmpSource {
if !m.Movie.MovieBase.Proxy || m.Movie.MovieBase.Live || m.Movie.MovieBase.RtmpSource {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
return
}
switch m.Movie.Base.Type {
switch m.Movie.MovieBase.Type {
case "mpd":
// TODO: cache mpd file
fallthrough
default:
err = proxyURL(ctx, m.Movie.Base.Url, m.Movie.Base.Headers)
err = proxyURL(ctx, m.Movie.MovieBase.Url, m.Movie.MovieBase.Headers)
if err != nil {
log.Errorf("proxy movie error: %v", err)
return
@ -614,11 +838,11 @@ func JoinLive(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
return
}
if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
log.Error("join live error: rtmp is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
return
} else if m.Movie.Base.Live && !settings.LiveProxy.Get() {
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
log.Error("join live error: live proxy is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
return
@ -632,7 +856,7 @@ func JoinLive(ctx *gin.Context) {
joinType := ctx.DefaultQuery("type", "auto")
if joinType == "auto" {
joinType = m.Movie.Base.Type
joinType = m.Movie.MovieBase.Type
}
switch joinType {
case "flv":
@ -678,11 +902,11 @@ func JoinFlvLive(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
return
}
if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
log.Error("join flv live error: rtmp is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
return
} else if m.Movie.Base.Live && !settings.LiveProxy.Get() {
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
log.Error("join flv live error: live proxy is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
return
@ -718,11 +942,11 @@ func JoinHlsLive(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
return
}
if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
log.Error("join hls live error: rtmp is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
return
} else if m.Movie.Base.Live && !settings.LiveProxy.Get() {
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
log.Error("join hls live error: live proxy is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
return
@ -768,11 +992,11 @@ func ServeHlsLive(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
return
}
if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
log.Error("serve hls live error: rtmp is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
return
} else if m.Movie.Base.Live && !settings.LiveProxy.Get() {
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
log.Error("serve hls live error: live proxy is not enabled")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
return
@ -833,9 +1057,9 @@ func ServeHlsLive(ctx *gin.Context) {
func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
log := ctx.MustGet("log").(*logrus.Entry)
switch movie.Movie.Base.VendorInfo.Vendor {
switch movie.Movie.MovieBase.VendorInfo.Vendor {
case dbModel.VendorBilibili:
if movie.Base.Live {
if movie.MovieBase.Live {
data, err := movie.BilibiliCache().Live.Get(ctx)
if err != nil {
log.Errorf("proxy vendor movie error: %v", err)
@ -853,7 +1077,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
t := ctx.Query("t")
switch t {
case "", "hevc":
if !movie.Movie.Base.Proxy {
if !movie.Movie.MovieBase.Proxy {
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
return
@ -901,7 +1125,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("stream id out of range"))
return
}
headers := maps.Clone(movie.Movie.Base.Headers)
headers := maps.Clone(movie.Movie.MovieBase.Headers)
if headers == nil {
headers = map[string]string{
"Referer": "https://www.bilibili.com",
@ -1006,7 +1230,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
case cache.AlistProvider115:
fallthrough
default:
if !movie.Movie.Base.Proxy {
if !movie.Movie.MovieBase.Proxy {
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
return
@ -1024,7 +1248,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
t := ctx.Query("t")
switch t {
case "":
if !movie.Movie.Base.Proxy {
if !movie.Movie.MovieBase.Proxy {
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
return
@ -1126,20 +1350,24 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
}
// user is the api requester
func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userAgent string, userToken string) (*dbModel.Movie, error) {
func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userAgent, userToken string) (*dbModel.Movie, error) {
movie := *opMovie.Movie
var err error
switch movie.Base.VendorInfo.Vendor {
switch movie.MovieBase.VendorInfo.Vendor {
case dbModel.VendorBilibili:
if movie.IsFolder {
return nil, fmt.Errorf("bilibili folder not support")
}
bmc := opMovie.BilibiliCache()
if movie.Base.Live {
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Type = "m3u8"
if movie.MovieBase.Live {
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Type = "m3u8"
return &movie, nil
} else {
if !movie.Base.Proxy {
if !movie.MovieBase.Proxy {
var s string
if movie.Base.VendorInfo.Bilibili.Shared {
if movie.MovieBase.VendorInfo.Bilibili.Shared {
var u *op.UserEntry
u, err = op.LoadOrInitUserByID(movie.CreatorID)
if err != nil {
@ -1153,11 +1381,11 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
return nil, err
}
movie.Base.Url = s
movie.MovieBase.Url = s
} else {
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Type = "mpd"
movie.Base.MoreSource = map[string]string{
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Type = "mpd"
movie.MovieBase.MoreSource = map[string]string{
"hevc": fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s&t=hevc", movie.RoomID, movie.ID, userToken),
}
}
@ -1166,10 +1394,10 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
return nil, err
}
for k := range srt {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(srt))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(srt))
}
movie.Base.Subtitles[k] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[k] = &dbModel.Subtitle{
URL: fmt.Sprintf("/api/movie/proxy/%s/%s?t=subtitle&n=%s&token=%s", movie.RoomID, movie.ID, k, userToken),
Type: "srt",
}
@ -1192,10 +1420,10 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
}
for _, subt := range data.Subtitles {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
}
movie.Base.Subtitles[subt.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
URL: subt.URL,
Type: subt.Type,
}
@ -1203,23 +1431,23 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
switch data.Provider {
case cache.AlistProviderAli:
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Type = "m3u8"
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Type = "m3u8"
for i, subt := range data.Subtitles {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
}
movie.Base.Subtitles[subt.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
URL: fmt.Sprintf("/api/movie/proxy/%s/%s?t=subtitle&id=%d&token=%s", movie.RoomID, movie.ID, i, userToken),
Type: subt.Type,
}
}
case cache.AlistProvider115:
if movie.Base.Proxy {
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Type = utils.GetUrlExtension(data.URL)
if movie.MovieBase.Proxy {
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Type = utils.GetUrlExtension(data.URL)
// TODO: proxy subtitle
} else {
@ -1230,10 +1458,10 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
if err != nil {
return nil, fmt.Errorf("refresh 115 movie cache error: %w", err)
}
movie.Base.Url = data.URL
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
movie.MovieBase.Url = data.URL
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
for _, subt := range data.Subtitles {
movie.Base.Subtitles[subt.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
URL: subt.URL,
Type: subt.Type,
}
@ -1241,15 +1469,15 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
}
default:
if !movie.Base.Proxy {
movie.Base.Url = data.URL
if !movie.MovieBase.Proxy {
movie.MovieBase.Url = data.URL
} else {
movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.Base.Type = utils.GetUrlExtension(data.URL)
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
movie.MovieBase.Type = utils.GetUrlExtension(data.URL)
}
}
movie.Base.VendorInfo.Alist.Password = ""
movie.MovieBase.VendorInfo.Alist.Password = ""
return &movie, nil
case dbModel.VendorEmby:
@ -1262,31 +1490,31 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
return nil, err
}
if !movie.Base.Proxy {
if !movie.MovieBase.Proxy {
if len(data.Sources) == 0 {
return nil, errors.New("no source")
}
movie.Base.Url = data.Sources[0].URL
movie.MovieBase.Url = data.Sources[0].URL
for _, s := range data.Sources[0].Subtitles {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Sources[0].Subtitles))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Sources[0].Subtitles))
}
movie.Base.Subtitles[s.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[s.Name] = &dbModel.Subtitle{
URL: s.URL,
Type: s.Type,
}
}
for _, s := range data.Sources[1:] {
if movie.Base.MoreSource == nil {
movie.Base.MoreSource = make(map[string]string, len(data.Sources)-1)
if movie.MovieBase.MoreSource == nil {
movie.MovieBase.MoreSource = make(map[string]string, len(data.Sources)-1)
}
movie.Base.MoreSource[s.Name] = s.URL
movie.MovieBase.MoreSource[s.Name] = s.URL
for _, subt := range s.Subtitles {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(s.Subtitles))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(s.Subtitles))
}
movie.Base.Subtitles[subt.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
URL: subt.URL,
Type: subt.Type,
}
@ -1298,7 +1526,7 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
if si != len(data.Sources)-1 {
continue
}
if movie.Base.Url == "" {
if movie.MovieBase.Url == "" {
return nil, errors.New("no source")
}
}
@ -1314,15 +1542,15 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
Path: rawPath,
RawQuery: rawQuery.Encode(),
}
movie.Base.Url = u.String()
movie.Base.Type = utils.GetUrlExtension(es.URL)
movie.MovieBase.Url = u.String()
movie.MovieBase.Type = utils.GetUrlExtension(es.URL)
if len(es.Subtitles) == 0 {
continue
}
for sbi, s := range es.Subtitles {
if movie.Base.Subtitles == nil {
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles))
if movie.MovieBase.Subtitles == nil {
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles))
}
rawQuery := url.Values{}
rawQuery.Set("t", "subtitle")
@ -1333,7 +1561,7 @@ func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userA
Path: rawPath,
RawQuery: rawQuery.Encode(),
}
movie.Base.Subtitles[s.Name] = &dbModel.Subtitle{
movie.MovieBase.Subtitles[s.Name] = &dbModel.Subtitle{
URL: u.String(),
Type: s.Type,
}

@ -32,7 +32,7 @@ func Me(ctx *gin.Context) {
Username: user.Username,
Role: user.Role,
CreatedAt: user.CreatedAt.UnixMilli(),
Email: user.Email.String,
Email: user.Email.String(),
}))
}
@ -304,7 +304,7 @@ func SendUserBindEmailCaptcha(ctx *gin.Context) {
return
}
if user.Email.String == req.Email {
if user.Email.String() == req.Email {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("this email same as current email"))
return
}

@ -238,8 +238,8 @@ func handleElementMsg(cli *op.Client, msg *pb.ElementMessage) error {
})
case pb.ElementMessageType_CHECK_EXPIRED:
current := cli.Room().Current()
if msg.ExpireId != 0 && current.MovieID != "" {
currentMovie, err := cli.Room().GetMovieByID(current.MovieID)
if msg.ExpireId != 0 && current.Movie.ID != "" {
currentMovie, err := cli.Room().GetMovieByID(current.Movie.ID)
if err != nil {
return cli.Send(&pb.ElementMessage{
Type: pb.ElementMessageType_ERROR,

@ -2,6 +2,8 @@ package model
import (
"errors"
"fmt"
"strings"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
@ -20,7 +22,7 @@ var (
ErrEmptyIds = errors.New("empty ids")
)
type PushMovieReq model.BaseMovie
type PushMovieReq model.MovieBase
func (p *PushMovieReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(p)
@ -90,6 +92,15 @@ func (i *IdCanEmptyReq) Validate() error {
return nil
}
type SetRoomCurrentMovieReq struct {
IdCanEmptyReq
SubPath string `json:"subPath"`
}
func (s *SetRoomCurrentMovieReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(s)
}
type EditMovieReq struct {
IdReq
PushMovieReq
@ -145,16 +156,72 @@ func (s *SwapMovieReq) Validate() error {
return nil
}
type MovieResp struct {
func GenDefaultSubPaths(path string, skipEmpty bool, paths ...*MoviePath) []*MoviePath {
if len(paths) == 0 {
return nil
}
id := paths[len(paths)-1].ID
path = strings.TrimRight(path, "/")
for _, v := range strings.Split(path, `/`) {
if v == "" && skipEmpty {
continue
}
if l := len(paths); l != 0 {
paths = append(paths, &MoviePath{
Name: v,
ID: id,
SubPath: fmt.Sprintf("%s/%s", strings.TrimRight(paths[l-1].SubPath, "/"), v),
})
} else {
paths = append(paths, &MoviePath{
Name: v,
ID: id,
SubPath: v,
})
}
}
return paths
}
type MoviePath struct {
Name string `json:"name"`
ID string `json:"id"`
SubPath string `json:"subPath"`
}
type MoviesResp struct {
Paths []*MoviePath `json:"paths"`
Movies []*Movie `json:"movies"`
Total int64 `json:"total"`
Dynamic bool `json:"dynamic"`
}
type Movie struct {
Id string `json:"id"`
CreatedAt int64 `json:"createAt"`
Base model.BaseMovie `json:"base"`
Base model.MovieBase `json:"base"`
Creator string `json:"creator"`
CreatorId string `json:"creatorId"`
SubPath string `json:"subPath"`
}
type CurrentMovieResp struct {
Status op.Status `json:"status"`
Movie *MovieResp `json:"movie"`
ExpireId uint64 `json:"expireId"`
Status op.Status `json:"status"`
Movie *Movie `json:"movie"`
ExpireId uint64 `json:"expireId"`
}
type ClearMoviesReq struct {
ParentId string `json:"parentId"`
}
func (c *ClearMoviesReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(c)
}
func (c *ClearMoviesReq) Validate() error {
if c.ParentId != "" && len(c.ParentId) != 32 {
return fmt.Errorf("parent id length must be empty or 32")
}
return nil
}

@ -23,7 +23,7 @@ type VendorFSListResp[T any] struct {
func GenDefaultPaths(path string, skipEmpty bool, paths ...*Path) []*Path {
path = strings.TrimRight(path, "/")
for _, v := range strings.Split(path, `/`) {
if skipEmpty && v == "" {
if v == "" && skipEmpty {
continue
}
if l := len(paths); l != 0 {

Loading…
Cancel
Save