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.
synctv/internal/cache/emby.go

347 lines
7.5 KiB
Go

package cache
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
12 months ago
"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
12 months ago
APIKey string
UserID string
Backend string
}
func NewEmbyUserCache(userID string) *EmbyUserCache {
5 months ago
return newMapCache0(func(_ 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")
}
1 month ago
v, err := db.GetEmbyVendor(userID, serverID)
if err != nil {
return nil, err
}
1 month ago
12 months ago
if v.APIKey == "" || v.Host == "" {
return nil, db.NotFoundError(db.ErrVendorNotFound)
}
1 month ago
return &EmbyUserCacheData{
Host: v.Host,
ServerID: v.ServerID,
12 months ago
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
}
5 months ago
func NewEmbyMovieClearCacheFunc(
movie *model.Movie,
_ string,
) func(ctx context.Context, args *EmbyUserCache) error {
return func(ctx context.Context, args *EmbyUserCache) error {
5 months ago
if !movie.VendorInfo.Emby.Transcode {
return nil
}
1 month ago
if args == nil {
return errors.New("need emby user cache")
}
5 months ago
serverID, err := movie.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
}
1 month ago
12 months ago
if aucd.Host == "" || aucd.APIKey == "" {
return errors.New("not bind emby vendor")
}
1 month ago
cli := vendor.LoadEmbyClient(aucd.Backend)
1 month ago
_, err = cli.DeleteActiveEncodeings(ctx, &emby.DeleteActiveEncodeingsReq{
Host: aucd.Host,
12 months ago
Token: aucd.APIKey,
PalySessionId: oldVal.TranscodeSessionID,
})
if err != nil {
log.Errorf("delete active encodeings: %v", err)
}
1 month ago
return nil
}
}
5 months ago
func NewEmbyMovieCacheInitFunc(
movie *model.Movie,
subPath string,
) func(ctx context.Context, args *EmbyUserCache) (*EmbyMovieCacheData, error) {
return func(ctx context.Context, args *EmbyUserCache) (*EmbyMovieCacheData, error) {
12 months ago
if err := validateEmbyArgs(args, movie, subPath); err != nil {
return nil, err
}
12 months ago
serverID, truePath, err := getEmbyServerIDAndPath(movie, subPath)
if err != nil {
return nil, err
}
aucd, err := args.LoadOrStore(ctx, serverID)
if err != nil {
return nil, err
}
1 month ago
12 months ago
if aucd.Host == "" || aucd.APIKey == "" {
return nil, errors.New("not bind emby vendor")
}
12 months ago
data, err := getPlaybackInfo(ctx, aucd, truePath)
if err != nil {
12 months ago
return nil, err
}
12 months ago
resp := &EmbyMovieCacheData{
5 months ago
Sources: make([]EmbySource, len(data.GetMediaSourceInfo())),
TranscodeSessionID: data.GetPlaySessionID(),
}
12 months ago
u, err := url.Parse(aucd.Host)
if err != nil {
return nil, err
}
12 months ago
5 months ago
for i, v := range data.GetMediaSourceInfo() {
12 months ago
source, err := processMediaSource(v, movie, aucd, truePath, u)
if err != nil {
return nil, err
}
1 month ago
12 months ago
if source != nil {
resp.Sources[i] = *source
resp.Sources[i].Subtitles = processEmbySubtitles(v, truePath, u)
}
}
12 months ago
return resp, nil
}
}
func validateEmbyArgs(args *EmbyUserCache, movie *model.Movie, subPath string) error {
if args == nil {
return errors.New("need emby user cache")
}
1 month ago
12 months ago
if movie.IsFolder && subPath == "" {
return errors.New("sub path is empty")
}
1 month ago
12 months ago
return nil
}
func getEmbyServerIDAndPath(movie *model.Movie, subPath string) (string, string, error) {
5 months ago
serverID, truePath, err := movie.VendorInfo.Emby.ServerIDAndFilePath()
12 months ago
if err != nil {
return "", "", err
}
1 month ago
12 months ago
if movie.IsFolder {
truePath = subPath
}
1 month ago
12 months ago
return serverID, truePath, nil
}
5 months ago
func getPlaybackInfo(
ctx context.Context,
aucd *EmbyUserCacheData,
truePath string,
) (*emby.PlaybackInfoResp, error) {
12 months ago
cli := vendor.LoadEmbyClient(aucd.Backend)
1 month ago
12 months ago
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)
}
1 month ago
12 months ago
return data, nil
}
5 months ago
func processMediaSource(
v *emby.MediaSourceInfo,
_ *model.Movie,
aucd *EmbyUserCacheData,
truePath string,
u *url.URL,
) (*EmbySource, error) {
source := &EmbySource{Name: v.GetName()}
switch {
case v.GetTranscodingUrl() != "":
source.URL = fmt.Sprintf("%s/emby%s", aucd.Host, v.GetTranscodingUrl())
12 months ago
source.IsTranscode = true
5 months ago
case v.GetDirectPlayUrl() != "":
source.URL = fmt.Sprintf("%s/emby%s", aucd.Host, v.GetDirectPlayUrl())
12 months ago
source.IsTranscode = false
5 months ago
default:
if v.GetContainer() == "" {
12 months ago
return nil, nil
}
1 month ago
5 months ago
result, err := url.JoinPath("emby", "Videos", truePath, "stream."+v.GetContainer())
12 months ago
if err != nil {
return nil, err
}
1 month ago
12 months ago
u.Path = result
query := url.Values{}
query.Set("api_key", aucd.APIKey)
query.Set("Static", "true")
5 months ago
query.Set("MediaSourceId", v.GetId())
12 months ago
u.RawQuery = query.Encode()
source.URL = u.String()
}
return source, nil
}
5 months ago
func processEmbySubtitles(
v *emby.MediaSourceInfo,
truePath string,
u *url.URL,
) []*EmbySubtitleCache {
subtitles := make([]*EmbySubtitleCache, 0, len(v.GetMediaStreamInfo()))
for _, msi := range v.GetMediaStreamInfo() {
if msi.GetType() != "Subtitle" {
12 months ago
continue
}
subtutleType := "srt"
1 month ago
5 months ago
result, err := url.JoinPath(
"emby",
"Videos",
truePath,
v.GetId(),
"Subtitles",
strconv.FormatUint(msi.GetIndex(), 10),
"Stream."+subtutleType,
)
12 months ago
if err != nil {
continue
}
1 month ago
12 months ago
u.Path = result
u.RawQuery = ""
url := u.String()
5 months ago
name := msi.GetDisplayTitle()
12 months ago
if name == "" {
5 months ago
if msi.GetTitle() != "" {
name = msi.GetTitle()
12 months ago
} else {
5 months ago
name = msi.GetDisplayLanguage()
12 months ago
}
}
subtitles = append(subtitles, &EmbySubtitleCache{
URL: url,
Type: subtutleType,
Name: name,
Cache: refreshcache0.NewRefreshCache(newEmbySubtitleCacheInitFunc(url), -1),
})
}
1 month ago
12 months ago
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
}
1 month ago
req.Header.Set("User-Agent", utils.UA)
req.Header.Set("Referer", req.URL.Host)
1 month ago
resp, err := uhc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
1 month ago
if resp.StatusCode != http.StatusOK {
return nil, errors.New("bad status code")
}
1 month ago
return io.ReadAll(resp.Body)
}
}