Feat: support emby

pull/45/head
zijiren233 3 years ago
parent 1ac9260c04
commit ee03ede641

@ -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

@ -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=

@ -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
}
}

@ -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"`
}

@ -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

@ -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")
}
}

Loading…
Cancel
Save