From ee03ede641d907618558291bbf88460cb14c882f Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Wed, 20 Dec 2023 01:12:06 +0800 Subject: [PATCH] Feat: support emby --- go.mod | 2 +- go.sum | 2 + internal/cache/emby.go | 99 ++++++++++++++++++++++++++++++++++++++++ internal/model/movie.go | 5 ++ internal/op/movie.go | 30 ++++++++---- server/handlers/movie.go | 48 ++++++++++++++++++- 6 files changed, 175 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 8c645d5..a559a2f 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/soheilhy/cmux v0.1.5 github.com/spf13/cobra v1.8.0 - github.com/synctv-org/vendors v0.1.1-0.20231219141441-14783f13d7cf + github.com/synctv-org/vendors v0.1.1-0.20231219160707-b86154001009 github.com/ulule/limiter/v3 v3.11.2 github.com/zencoder/go-dash/v3 v3.0.3 github.com/zijiren233/gencontainer v0.0.0-20231213075414-f7f4c8261dca diff --git a/go.sum b/go.sum index 13abc98..45e1fdb 100644 --- a/go.sum +++ b/go.sum @@ -356,6 +356,8 @@ github.com/synctv-org/vendors v0.1.1-0.20231219132638-e5f7129a6b4a h1:AX89ME2jh0 github.com/synctv-org/vendors v0.1.1-0.20231219132638-e5f7129a6b4a/go.mod h1:C0ZGPeF8nYsx60gePPhSrgrjrONj6F72XFHW9Mh8+vU= github.com/synctv-org/vendors v0.1.1-0.20231219141441-14783f13d7cf h1:NtoqCZWMUjbIENKQnaLmw/cT2Wc8Fu7C212bc/EDtV0= github.com/synctv-org/vendors v0.1.1-0.20231219141441-14783f13d7cf/go.mod h1:C0ZGPeF8nYsx60gePPhSrgrjrONj6F72XFHW9Mh8+vU= +github.com/synctv-org/vendors v0.1.1-0.20231219160707-b86154001009 h1:iT6l+r8aPkjSWaXFYD1LrpaC+Vt6afpb3CvrynivyXw= +github.com/synctv-org/vendors v0.1.1-0.20231219160707-b86154001009/go.mod h1:C0ZGPeF8nYsx60gePPhSrgrjrONj6F72XFHW9Mh8+vU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= diff --git a/internal/cache/emby.go b/internal/cache/emby.go index 729fa8e..ca7f32a 100644 --- a/internal/cache/emby.go +++ b/internal/cache/emby.go @@ -3,8 +3,13 @@ package cache import ( "context" "errors" + "fmt" + "net/url" "github.com/synctv-org/synctv/internal/db" + "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/vendor" + "github.com/synctv-org/vendors/api/emby" "github.com/zijiren233/gencontainer/refreshcache" ) @@ -42,3 +47,97 @@ func EmbyAuthorizationCacheWithUserIDInitFunc(userID string) func(ctx context.Co }, nil } } + +type EmbySource struct { + URLs []struct { + URL string + Name string + } + // TODO: cache subtitles + Subtitles []struct { + URL string + Name string + } +} + +type EmbyMovieCacheData struct { + Sources []EmbySource +} + +type EmbyMovieCache = refreshcache.RefreshCache[*EmbyMovieCacheData, *EmbyUserCache] + +func NewEmbyMovieCache(movie *model.Movie) *EmbyMovieCache { + return refreshcache.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie), 0) +} + +func NewEmbyMovieCacheInitFunc(movie *model.Movie) 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") + } + aucd, err := args[0].Get(ctx) + if err != nil { + return nil, err + } + if aucd.Host == "" || aucd.ApiKey == "" { + return nil, errors.New("not bind emby vendor") + } + u, err := url.Parse(aucd.Host) + if err != nil { + return nil, err + } + cli := vendor.LoadEmbyClient(aucd.Backend) + data, err := cli.GetItem(ctx, &emby.GetItemReq{ + Host: aucd.Host, + Token: aucd.ApiKey, + ItemId: movie.Base.VendorInfo.Emby.Path, + }) + if err != nil { + return nil, err + } + if data.IsFolder { + return nil, errors.New("path is dir") + } + var resp EmbyMovieCacheData = EmbyMovieCacheData{ + Sources: make([]EmbySource, len(data.MediaSourceInfo)), + } + for i, v := range data.MediaSourceInfo { + result, err := url.JoinPath("emby", "Videos", data.Id, fmt.Sprintf("stream.%s", v.Container)) + if err != nil { + return nil, err + } + u.Path = result + query := url.Values{} + query.Set("api_key", aucd.ApiKey) + query.Set("Static", "true") + query.Set("MediaSourceId", v.Id) + u.RawQuery = query.Encode() + resp.Sources[i].URLs = append(resp.Sources[i].URLs, struct { + URL string + Name string + }{ + URL: u.String(), + Name: v.Name, + }) + for _, msi := range v.MediaStreamInfo { + switch msi.Type { + case "Subtitle": + result, err = url.JoinPath("emby", "Videos", data.Id, v.Id, "Subtitles", fmt.Sprintf("%d", msi.Index), "Stream.srt") + if err != nil { + return nil, err + } + u.Path = result + u.RawQuery = "" + resp.Sources[i].Subtitles = append(resp.Sources[i].Subtitles, struct { + URL string + Name string + }{ + URL: u.String(), + Name: msi.DisplayTitle, + }) + } + } + } + return &resp, nil + } +} diff --git a/internal/model/movie.go b/internal/model/movie.go index 08a766c..c358159 100644 --- a/internal/model/movie.go +++ b/internal/model/movie.go @@ -55,6 +55,7 @@ type VendorInfo struct { Backend string `json:"backend"` Bilibili *BilibiliStreamingInfo `gorm:"embedded;embeddedPrefix:bilibili_" json:"bilibili,omitempty"` Alist *AlistStreamingInfo `gorm:"embedded;embeddedPrefix:alist_" json:"alist,omitempty"` + Emby *EmbyStreamingInfo `gorm:"embedded;embeddedPrefix:emby_" json:"emby,omitempty"` } type BilibiliStreamingInfo struct { @@ -113,3 +114,7 @@ func (a *AlistStreamingInfo) AfterSave(tx *gorm.DB) error { func (a *AlistStreamingInfo) AfterFind(tx *gorm.DB) error { return a.AfterSave(tx) } + +type EmbyStreamingInfo struct { + Path string `json:"path,omitempty"` +} diff --git a/internal/op/movie.go b/internal/op/movie.go index 714ec11..ce533c6 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -24,8 +24,20 @@ import ( type Movie struct { Movie model.Movie channel atomic.Pointer[rtmps.Channel] - bilibiliCache atomic.Pointer[cache.BilibiliMovieCache] alistCache atomic.Pointer[cache.AlistMovieCache] + bilibiliCache atomic.Pointer[cache.BilibiliMovieCache] + embyCache atomic.Pointer[cache.EmbyMovieCache] +} + +func (m *Movie) AlistCache() *cache.AlistMovieCache { + c := m.alistCache.Load() + if c == nil { + c = cache.NewAlistMovieCache(&m.Movie) + if !m.alistCache.CompareAndSwap(nil, c) { + return m.AlistCache() + } + } + return c } func (m *Movie) BilibiliCache() *cache.BilibiliMovieCache { @@ -39,12 +51,12 @@ func (m *Movie) BilibiliCache() *cache.BilibiliMovieCache { return c } -func (m *Movie) AlistCache() *cache.AlistMovieCache { - c := m.alistCache.Load() +func (m *Movie) EmbyCache() *cache.EmbyMovieCache { + c := m.embyCache.Load() if c == nil { - c = cache.NewAlistMovieCache(&m.Movie) - if !m.alistCache.CompareAndSwap(nil, c) { - return m.AlistCache() + c = cache.NewEmbyMovieCache(&m.Movie) + if !m.embyCache.CompareAndSwap(nil, c) { + return m.EmbyCache() } } return c @@ -213,10 +225,12 @@ func (movie *Movie) validateVendorMovie() error { return movie.Movie.Base.VendorInfo.Bilibili.Validate() case model.VendorAlist: - // return movie.Movie.Base.VendorInfo.Alist.Validate() + // return movie.Movie.Base.VendorInfo.Alist.Validate() + + case model.VendorEmby: default: - return fmt.Errorf("vendor not support") + return fmt.Errorf("vendor not implement validate") } return nil diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 5ca3684..31614bd 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -732,11 +732,12 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { } default: - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support")) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support proxy")) return } } +// user is the api requester func parse2VendorMovie(ctx context.Context, user *op.User, room *op.Room, movie *dbModel.Movie) (err error) { switch movie.Base.VendorInfo.Vendor { case dbModel.VendorBilibili: @@ -794,11 +795,54 @@ func parse2VendorMovie(ctx context.Context, user *op.User, room *op.Room, movie return err } + // TODO: when proxy movie.Base.Url = data.URLs[len(data.URLs)-1].URL movie.Base.VendorInfo.Alist = nil return nil + case dbModel.VendorEmby: + u, err := op.LoadOrInitUserByID(movie.CreatorID) + if err != nil { + return err + } + opM, err := room.GetMovieByID(movie.ID) + if err != nil { + return err + } + data, err := opM.EmbyCache().Get(ctx, u.EmbyCache()) + if err != nil { + return err + } + + // TODO: when proxy + for i, es := range data.Sources { + if len(es.URLs) == 0 { + if i != len(data.Sources)-1 { + continue + } + if movie.Base.Url == "" { + return errors.New("no source") + } + } + movie.Base.Url = es.URLs[0].URL + + if len(es.Subtitles) == 0 { + continue + } + for _, s := range es.Subtitles { + if movie.Base.Subtitles == nil { + movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles)) + } + movie.Base.Subtitles[s.Name] = &dbModel.Subtitle{ + URL: s.URL, + Type: "srt", + } + } + } + movie.Base.VendorInfo.Emby = nil + return nil + default: - return fmt.Errorf("vendor not support") + return fmt.Errorf("vendor not implement gen movie url") } }