mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
7.4 KiB
Go
293 lines
7.4 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
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"
|
|
"github.com/synctv-org/synctv/utils"
|
|
"github.com/synctv-org/vendors/api/emby"
|
|
"github.com/zijiren233/gencontainer/refreshcache"
|
|
"github.com/zijiren233/gencontainer/refreshcache0"
|
|
"github.com/zijiren233/gencontainer/refreshcache1"
|
|
"github.com/zijiren233/go-uhc"
|
|
)
|
|
|
|
type EmbyUserCache = MapCache0[*EmbyUserCacheData]
|
|
|
|
type EmbyUserCacheData struct {
|
|
Host string
|
|
ServerID string
|
|
APIKey string
|
|
UserID string
|
|
Backend string
|
|
}
|
|
|
|
func NewEmbyUserCache(userID string) *EmbyUserCache {
|
|
return newMapCache0(func(ctx context.Context, key string) (*EmbyUserCacheData, error) {
|
|
return EmbyAuthorizationCacheWithUserIDInitFunc(userID, key)
|
|
}, -1)
|
|
}
|
|
|
|
func EmbyAuthorizationCacheWithUserIDInitFunc(userID, serverID string) (*EmbyUserCacheData, error) {
|
|
if serverID == "" {
|
|
return nil, errors.New("serverID is required")
|
|
}
|
|
v, err := db.GetEmbyVendor(userID, serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if v.APIKey == "" || v.Host == "" {
|
|
return nil, db.NotFoundError(db.ErrVendorNotFound)
|
|
}
|
|
return &EmbyUserCacheData{
|
|
Host: v.Host,
|
|
ServerID: v.ServerID,
|
|
APIKey: v.APIKey,
|
|
UserID: v.EmbyUserID,
|
|
Backend: v.Backend,
|
|
}, nil
|
|
}
|
|
|
|
type EmbySource struct {
|
|
URL string
|
|
Name string
|
|
Subtitles []*EmbySubtitleCache
|
|
IsTranscode bool
|
|
}
|
|
|
|
type EmbySubtitleCache struct {
|
|
Cache *refreshcache0.RefreshCache[[]byte]
|
|
URL string
|
|
Type string
|
|
Name string
|
|
}
|
|
|
|
type EmbyMovieCacheData struct {
|
|
TranscodeSessionID string
|
|
Sources []EmbySource
|
|
}
|
|
|
|
type EmbyMovieCache = refreshcache1.RefreshCache[*EmbyMovieCacheData, *EmbyUserCache]
|
|
|
|
func NewEmbyMovieCache(movie *model.Movie, subPath string) *EmbyMovieCache {
|
|
cache := refreshcache1.NewRefreshCache(NewEmbyMovieCacheInitFunc(movie, subPath), -1)
|
|
cache.SetClearFunc(NewEmbyMovieClearCacheFunc(movie, subPath))
|
|
return cache
|
|
}
|
|
|
|
func NewEmbyMovieClearCacheFunc(movie *model.Movie, subPath string) func(ctx context.Context, args *EmbyUserCache) error {
|
|
return func(ctx context.Context, args *EmbyUserCache) error {
|
|
if !movie.MovieBase.VendorInfo.Emby.Transcode {
|
|
return nil
|
|
}
|
|
if args == nil {
|
|
return errors.New("need emby user cache")
|
|
}
|
|
|
|
serverID, err := movie.MovieBase.VendorInfo.Emby.ServerID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldVal, ok := ctx.Value(refreshcache.OldValKey).(*EmbyMovieCacheData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
aucd, err := args.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, subPath string) func(ctx context.Context, args *EmbyUserCache) (*EmbyMovieCacheData, error) {
|
|
return func(ctx context.Context, args *EmbyUserCache) (*EmbyMovieCacheData, error) {
|
|
if err := validateEmbyArgs(args, movie, subPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serverID, truePath, err := getEmbyServerIDAndPath(movie, subPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aucd, err := args.LoadOrStore(ctx, serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if aucd.Host == "" || aucd.APIKey == "" {
|
|
return nil, errors.New("not bind emby vendor")
|
|
}
|
|
|
|
data, err := getPlaybackInfo(ctx, aucd, truePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp := &EmbyMovieCacheData{
|
|
Sources: make([]EmbySource, len(data.MediaSourceInfo)),
|
|
TranscodeSessionID: data.PlaySessionID,
|
|
}
|
|
|
|
u, err := url.Parse(aucd.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, v := range data.MediaSourceInfo {
|
|
source, err := processMediaSource(v, movie, aucd, truePath, u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if source != nil {
|
|
resp.Sources[i] = *source
|
|
resp.Sources[i].Subtitles = processEmbySubtitles(v, truePath, u)
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
func validateEmbyArgs(args *EmbyUserCache, movie *model.Movie, subPath string) error {
|
|
if args == nil {
|
|
return errors.New("need emby user cache")
|
|
}
|
|
if movie.IsFolder && subPath == "" {
|
|
return errors.New("sub path is empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getEmbyServerIDAndPath(movie *model.Movie, subPath string) (string, string, error) {
|
|
serverID, truePath, err := movie.MovieBase.VendorInfo.Emby.ServerIDAndFilePath()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if movie.IsFolder {
|
|
truePath = subPath
|
|
}
|
|
return serverID, truePath, nil
|
|
}
|
|
|
|
func getPlaybackInfo(ctx context.Context, aucd *EmbyUserCacheData, truePath string) (*emby.PlaybackInfoResp, error) {
|
|
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)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func processMediaSource(v *emby.MediaSourceInfo, movie *model.Movie, aucd *EmbyUserCacheData, truePath string, u *url.URL) (*EmbySource, error) {
|
|
source := &EmbySource{Name: v.Name}
|
|
|
|
if v.TranscodingUrl != "" {
|
|
source.URL = fmt.Sprintf("%s/emby%s", aucd.Host, v.TranscodingUrl)
|
|
source.IsTranscode = true
|
|
} else if v.DirectPlayUrl != "" {
|
|
source.URL = fmt.Sprintf("%s/emby%s", aucd.Host, v.DirectPlayUrl)
|
|
source.IsTranscode = false
|
|
} else {
|
|
if v.Container == "" {
|
|
return nil, nil
|
|
}
|
|
result, err := url.JoinPath("emby", "Videos", truePath, "stream."+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()
|
|
source.URL = u.String()
|
|
}
|
|
|
|
return source, nil
|
|
}
|
|
|
|
func processEmbySubtitles(v *emby.MediaSourceInfo, truePath string, u *url.URL) []*EmbySubtitleCache {
|
|
subtitles := make([]*EmbySubtitleCache, 0, len(v.MediaStreamInfo))
|
|
for _, msi := range v.MediaStreamInfo {
|
|
if msi.Type != "Subtitle" {
|
|
continue
|
|
}
|
|
|
|
subtutleType := "srt"
|
|
result, err := url.JoinPath("emby", "Videos", truePath, v.Id, "Subtitles", strconv.Itoa(int(msi.Index)), "Stream."+subtutleType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
u.Path = result
|
|
u.RawQuery = ""
|
|
url := u.String()
|
|
|
|
name := msi.DisplayTitle
|
|
if name == "" {
|
|
if msi.Title != "" {
|
|
name = msi.Title
|
|
} else {
|
|
name = msi.DisplayLanguage
|
|
}
|
|
}
|
|
|
|
subtitles = append(subtitles, &EmbySubtitleCache{
|
|
URL: url,
|
|
Type: subtutleType,
|
|
Name: name,
|
|
Cache: refreshcache0.NewRefreshCache(newEmbySubtitleCacheInitFunc(url), -1),
|
|
})
|
|
}
|
|
return subtitles
|
|
}
|
|
|
|
func newEmbySubtitleCacheInitFunc(url string) func(ctx context.Context) ([]byte, error) {
|
|
return func(ctx context.Context) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", utils.UA)
|
|
req.Header.Set("Referer", req.URL.Host)
|
|
resp, err := uhc.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, errors.New("bad status code")
|
|
}
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
}
|