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