mirror of https://github.com/synctv-org/synctv.git
Feat: emby support transcode
This commit is contained in:
parent
b2b00ca3c0
commit
4cee111d04
4
go.mod
4
go.mod
|
@ -33,10 +33,10 @@ 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.3.3-0.20240418153252-2198604d72f2
|
||||
github.com/synctv-org/vendors v0.3.3-0.20240507143426-41159dcb43e3
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/zencoder/go-dash/v3 v3.0.3
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240331174346-b5e420773df7
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240507135401-7a9c8355bb2c
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb
|
||||
github.com/zijiren233/go-uhc v0.2.3
|
||||
github.com/zijiren233/livelib v0.3.1
|
||||
|
|
8
go.sum
8
go.sum
|
@ -365,8 +365,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/synctv-org/vendors v0.3.3-0.20240418153252-2198604d72f2 h1:EiooxruEJSuMlDe4PAD+mlMz8ChvSWeql4Z9kdc7/Rg=
|
||||
github.com/synctv-org/vendors v0.3.3-0.20240418153252-2198604d72f2/go.mod h1:8ONGycZGgULyvajYbmjwGo8fEN6g9c+Q1lhoeDn6rqY=
|
||||
github.com/synctv-org/vendors v0.3.3-0.20240507143426-41159dcb43e3 h1:+KV0W7224USi8KqXVG3aAmJQEAbumzLi9/UhwALvkAk=
|
||||
github.com/synctv-org/vendors v0.3.3-0.20240507143426-41159dcb43e3/go.mod h1:8ONGycZGgULyvajYbmjwGo8fEN6g9c+Q1lhoeDn6rqY=
|
||||
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
|
||||
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
|
@ -381,8 +381,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zencoder/go-dash/v3 v3.0.3 h1:xqwGJ2fJCSArwONGx6sY26Z1lxQ7zTURoxdRjCpuodM=
|
||||
github.com/zencoder/go-dash/v3 v3.0.3/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240331174346-b5e420773df7 h1:ymsEhM4NrTiZx/nyJb5CQRVOmdgZm6L6hHcRUErXYVQ=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240331174346-b5e420773df7/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240507135401-7a9c8355bb2c h1:xpQLguenF4bPTrbtCz+kMHpDtUDFIn/iZKd4VRfxe3Y=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20240507135401-7a9c8355bb2c/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/TbbGodHhOVHNoPk+7v/YBJACs22gKpKlatWw=
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb/go.mod h1:6TCzjDiQ8+5gWZiwsC3pnA5M0vUy2jV2Y7ciHJh729g=
|
||||
github.com/zijiren233/go-uhc v0.2.3 h1:mBtTqLpdFzGSmeSYYUn+y0w1J+qolVd8UoS6h6ROyUs=
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/db"
|
||||
"github.com/synctv-org/synctv/internal/model"
|
||||
"github.com/synctv-org/synctv/internal/vendor"
|
||||
|
@ -23,6 +24,7 @@ type EmbyUserCacheData struct {
|
|||
Host string
|
||||
ServerID string
|
||||
ApiKey string
|
||||
UserID string
|
||||
Backend string
|
||||
}
|
||||
|
||||
|
@ -47,14 +49,16 @@ func EmbyAuthorizationCacheWithUserIDInitFunc(userID, serverID string) (*EmbyUse
|
|||
Host: v.Host,
|
||||
ServerID: v.ServerID,
|
||||
ApiKey: v.ApiKey,
|
||||
UserID: v.UserID,
|
||||
Backend: v.Backend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type EmbySource struct {
|
||||
URLs []struct {
|
||||
URL string
|
||||
Name string
|
||||
URL string
|
||||
IsTranscode bool
|
||||
Name string
|
||||
}
|
||||
Subtitles []struct {
|
||||
URL string
|
||||
|
@ -65,13 +69,52 @@ type EmbySource struct {
|
|||
}
|
||||
|
||||
type EmbyMovieCacheData struct {
|
||||
Sources []EmbySource
|
||||
Sources []EmbySource
|
||||
TranscodeSessionID string
|
||||
}
|
||||
|
||||
type EmbyMovieCache = refreshcache.RefreshCache[*EmbyMovieCacheData, *EmbyUserCache]
|
||||
|
||||
func NewEmbyMovieCache(movie *model.Movie) *EmbyMovieCache {
|
||||
return refreshcache.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie), 0)
|
||||
cache := refreshcache.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie), 0)
|
||||
cache.SetClearFunc(NewEmbyMovieClearCacheFunc(movie))
|
||||
return cache
|
||||
}
|
||||
|
||||
func NewEmbyMovieClearCacheFunc(movie *model.Movie) func(ctx context.Context, args ...*EmbyUserCache) error {
|
||||
return func(ctx context.Context, args ...*MapCache[*EmbyUserCacheData, struct{}]) error {
|
||||
if !movie.Base.VendorInfo.Emby.Transcode {
|
||||
return nil
|
||||
}
|
||||
|
||||
serverID, _, err := model.GetEmbyServerIdFromPath(movie.Base.VendorInfo.Emby.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldVal, ok := ctx.Value(refreshcache.OldValKey).(*EmbyMovieCacheData)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
aucd, err := args[0].LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if aucd.Host == "" || aucd.ApiKey == "" {
|
||||
return errors.New("not bind emby vendor")
|
||||
}
|
||||
cli := vendor.LoadEmbyClient(aucd.Backend)
|
||||
_, err = cli.DeleteActiveEncodeings(ctx, &emby.DeleteActiveEncodeingsReq{
|
||||
Host: aucd.Host,
|
||||
Token: aucd.ApiKey,
|
||||
PalySessionId: oldVal.TranscodeSessionID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("delete active encodeings: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, args ...*EmbyUserCache) (*EmbyMovieCacheData, error) {
|
||||
|
@ -97,51 +140,72 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
|
|||
if aucd.Host == "" || aucd.ApiKey == "" {
|
||||
return nil, errors.New("not bind emby vendor")
|
||||
}
|
||||
cli := vendor.LoadEmbyClient(aucd.Backend)
|
||||
data, err := cli.PlaybackInfo(ctx, &emby.PlaybackInfoReq{
|
||||
Host: aucd.Host,
|
||||
Token: aucd.ApiKey,
|
||||
UserId: aucd.UserID,
|
||||
ItemId: truePath,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("playback info: %w", err)
|
||||
}
|
||||
var resp EmbyMovieCacheData = EmbyMovieCacheData{
|
||||
Sources: make([]EmbySource, len(data.MediaSourceInfo)),
|
||||
TranscodeSessionID: data.PlaySessionID,
|
||||
}
|
||||
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: truePath,
|
||||
})
|
||||
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 {
|
||||
if v.Container == "" {
|
||||
continue
|
||||
if movie.Base.VendorInfo.Emby.Transcode && v.TranscodingUrl != "" {
|
||||
resp.Sources[i].URLs = append(resp.Sources[i].URLs, struct {
|
||||
URL string
|
||||
IsTranscode bool
|
||||
Name string
|
||||
}{
|
||||
URL: fmt.Sprintf("%s/emby%s", aucd.Host, v.TranscodingUrl),
|
||||
Name: v.Name,
|
||||
IsTranscode: true,
|
||||
})
|
||||
} else if v.DirectPlayUrl != "" {
|
||||
resp.Sources[i].URLs = append(resp.Sources[i].URLs, struct {
|
||||
URL string
|
||||
IsTranscode bool
|
||||
Name string
|
||||
}{
|
||||
URL: fmt.Sprintf("%s/emby%s", aucd.Host, v.DirectPlayUrl),
|
||||
Name: v.Name,
|
||||
})
|
||||
} else {
|
||||
if v.Container == "" {
|
||||
continue
|
||||
}
|
||||
result, err := url.JoinPath("emby", "Videos", truePath, 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
|
||||
IsTranscode bool
|
||||
Name string
|
||||
}{
|
||||
URL: u.String(),
|
||||
Name: v.Name,
|
||||
})
|
||||
}
|
||||
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":
|
||||
subtutleType := "srt"
|
||||
result, err = url.JoinPath("emby", "Videos", data.Id, v.Id, "Subtitles", fmt.Sprintf("%d", msi.Index), fmt.Sprintf("Stream.%s", subtutleType))
|
||||
result, err := url.JoinPath("emby", "Videos", truePath, v.Id, "Subtitles", fmt.Sprintf("%d", msi.Index), fmt.Sprintf("Stream.%s", subtutleType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -136,7 +136,8 @@ func (a *AlistStreamingInfo) AfterFind(tx *gorm.DB) error {
|
|||
|
||||
type EmbyStreamingInfo struct {
|
||||
// {/}serverId/ItemId
|
||||
Path string `gorm:"type:varchar(52)" json:"path,omitempty"`
|
||||
Path string `gorm:"type:varchar(52)" json:"path,omitempty"`
|
||||
Transcode bool `json:"transcode,omitempty"`
|
||||
}
|
||||
|
||||
func GetEmbyServerIdFromPath(path string) (serverID string, filePath string, err error) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
|
@ -56,7 +57,7 @@ func (m *Movie) CheckExpired(expireId uint64) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Movie) ClearCache() {
|
||||
func (m *Movie) ClearCache() error {
|
||||
m.alistCache.Store(nil)
|
||||
|
||||
bmc := m.bilibiliCache.Swap(nil)
|
||||
|
@ -64,7 +65,19 @@ func (m *Movie) ClearCache() {
|
|||
bmc.NoSharedMovie.Clear()
|
||||
}
|
||||
|
||||
m.embyCache.Store(nil)
|
||||
emc := m.embyCache.Swap(nil)
|
||||
if emc != nil {
|
||||
u, err := LoadOrInitUserByID(m.CreatorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = emc.Clear(context.Background(), u.Value().EmbyCache())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Movie) AlistCache() *cache.AlistMovieCache {
|
||||
|
|
|
@ -387,6 +387,17 @@ func (r *Room) CheckCurrentExpired(expireId uint64) (bool, error) {
|
|||
}
|
||||
|
||||
func (r *Room) SetCurrentMovie(movieID string, play bool) error {
|
||||
currentMovie, err := r.CurrentMovie()
|
||||
if err != nil {
|
||||
if err != ErrNoCurrentMovie {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = currentMovie.ClearCache()
|
||||
if err != nil {
|
||||
return fmt.Errorf("clear cache failed: %w", err)
|
||||
}
|
||||
}
|
||||
if movieID == "" {
|
||||
r.current.SetMovie("", false, play)
|
||||
return nil
|
||||
|
|
|
@ -78,3 +78,11 @@ func (e *grpcEmby) Logout(ctx context.Context, req *emby.LogoutReq) (*emby.Empty
|
|||
func (e *grpcEmby) Me(ctx context.Context, req *emby.MeReq) (*emby.MeResp, error) {
|
||||
return e.client.Me(ctx, req)
|
||||
}
|
||||
|
||||
func (e *grpcEmby) PlaybackInfo(ctx context.Context, req *emby.PlaybackInfoReq) (*emby.PlaybackInfoResp, error) {
|
||||
return e.client.PlaybackInfo(ctx, req)
|
||||
}
|
||||
|
||||
func (e *grpcEmby) DeleteActiveEncodeings(ctx context.Context, req *emby.DeleteActiveEncodeingsReq) (*emby.Empty, error) {
|
||||
return e.client.DeleteActiveEncodeings(ctx, req)
|
||||
}
|
||||
|
|
|
@ -1040,6 +1040,10 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
|
|||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
|
||||
return
|
||||
}
|
||||
if embyC.Sources[source].URLs[id].IsTranscode {
|
||||
ctx.Redirect(http.StatusFound, embyC.Sources[source].URLs[id].URL)
|
||||
return
|
||||
}
|
||||
err = proxyURL(ctx, embyC.Sources[source].URLs[id].URL, nil)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
|
|
|
@ -115,7 +115,7 @@ func Logout(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
if rc, ok := user.AlistCache().LoadCache(req.ServerID); ok {
|
||||
rc.Clear()
|
||||
rc.Clear(ctx)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
|
|
@ -233,6 +233,6 @@ func Logout(ctx *gin.Context) {
|
|||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
user.BilibiliCache().Clear()
|
||||
user.BilibiliCache().Clear(ctx)
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -136,6 +136,7 @@ EmbyFSListResp:
|
|||
Host: aucd.Host,
|
||||
Path: req.Path,
|
||||
Token: aucd.ApiKey,
|
||||
UserId: aucd.UserID,
|
||||
Limit: uint64(size),
|
||||
StartIndex: uint64((page - 1) * size),
|
||||
SearchTerm: req.Keywords,
|
||||
|
|
Loading…
Reference in New Issue