Feat: emby support proxy and subtitle cache

zijiren233-patch-1
zijiren233 2 years ago
parent ee03ede641
commit eda5bcb8f7

@ -4,11 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"net/url" "net/url"
"github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/db"
"github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/vendor" "github.com/synctv-org/synctv/internal/vendor"
"github.com/synctv-org/synctv/utils"
"github.com/synctv-org/vendors/api/emby" "github.com/synctv-org/vendors/api/emby"
"github.com/zijiren233/gencontainer/refreshcache" "github.com/zijiren233/gencontainer/refreshcache"
) )
@ -55,8 +58,10 @@ type EmbySource struct {
} }
// TODO: cache subtitles // TODO: cache subtitles
Subtitles []struct { Subtitles []struct {
URL string URL string
Name string Type string
Name string
Cache *refreshcache.RefreshCache[[]byte, struct{}]
} }
} }
@ -122,18 +127,32 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
for _, msi := range v.MediaStreamInfo { for _, msi := range v.MediaStreamInfo {
switch msi.Type { switch msi.Type {
case "Subtitle": case "Subtitle":
result, err = url.JoinPath("emby", "Videos", data.Id, v.Id, "Subtitles", fmt.Sprintf("%d", msi.Index), "Stream.srt") subtutleType := "srt"
result, err = url.JoinPath("emby", "Videos", data.Id, v.Id, "Subtitles", fmt.Sprintf("%d", msi.Index), fmt.Sprintf("Stream.%s", subtutleType))
if err != nil { if err != nil {
return nil, err return nil, err
} }
u.Path = result u.Path = result
u.RawQuery = "" u.RawQuery = ""
url := u.String()
name := msi.DisplayTitle
if name == "" {
if msi.Title != "" {
name = msi.Title
} else {
name = msi.DisplayLanguage
}
}
resp.Sources[i].Subtitles = append(resp.Sources[i].Subtitles, struct { resp.Sources[i].Subtitles = append(resp.Sources[i].Subtitles, struct {
URL string URL string
Name string Type string
Name string
Cache *refreshcache.RefreshCache[[]byte, struct{}]
}{ }{
URL: u.String(), URL: url,
Name: msi.DisplayTitle, Type: subtutleType,
Name: name,
Cache: refreshcache.NewRefreshCache(newEmbySubtitleCacheInitFunc(url), 0),
}) })
} }
} }
@ -141,3 +160,23 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
return &resp, nil return &resp, nil
} }
} }
func newEmbySubtitleCacheInitFunc(url string) func(ctx context.Context, args ...struct{}) ([]byte, error) {
return func(ctx context.Context, args ...struct{}) ([]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 := http.DefaultClient.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)
}
}

@ -11,6 +11,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"path" "path"
"strconv" "strconv"
"strings" "strings"
@ -60,14 +61,14 @@ func MovieList(ctx *gin.Context) {
return return
} }
m := room.GetMoviesWithPage(page, max) current := room.Current()
err = genCurrent(ctx, user, room, current)
current, err := genCurrent(ctx, user, room, room.Current())
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
} }
m := room.GetMoviesWithPage(page, max)
mresp := make([]model.MoviesResp, len(m)) mresp := make([]model.MoviesResp, len(m))
for i, v := range m { for i, v := range m {
mresp[i] = model.MoviesResp{ mresp[i] = model.MoviesResp{
@ -91,37 +92,38 @@ func MovieList(ctx *gin.Context) {
})) }))
} }
func genCurrent(ctx context.Context, user *op.User, room *op.Room, current *op.Current) (*op.Current, error) { func genCurrent(ctx context.Context, user *op.User, room *op.Room, current *op.Current) error {
if current.Movie.Base.VendorInfo.Vendor != "" { if current.Movie.Base.VendorInfo.Vendor != "" {
return current, parse2VendorMovie(ctx, user, room, &current.Movie) return parse2VendorMovie(ctx, user, room, &current.Movie)
}
if current.Movie.Base.RtmpSource || current.Movie.Base.Live && current.Movie.Base.Proxy {
t := current.Movie.Base.Type
if t != "flv" && t != "m3u8" {
t = "m3u8"
}
current.Movie.Base.Url = fmt.Sprintf("/api/movie/live/%s.%s", current.Movie.ID, t)
current.Movie.Base.Headers = nil
} else if current.Movie.Base.Proxy {
current.Movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s", current.Movie.RoomID, current.Movie.ID)
current.Movie.Base.Headers = nil
} }
return current, nil if current.Movie.Base.Type == "" && current.Movie.Base.Url != "" {
current.Movie.Base.Type = utils.GetUrlExtension(current.Movie.Base.Url)
}
return nil
} }
func genCurrentResp(current *op.Current) *model.CurrentMovieResp { func genCurrentResp(current *op.Current) *model.CurrentMovieResp {
c := &model.CurrentMovieResp{ c := &model.CurrentMovieResp{
Status: current.Status, Status: current.Status,
Movie: model.MoviesResp{ Movie: model.MoviesResp{
Id: current.Movie.ID, Id: current.Movie.ID,
Base: current.Movie.Base, CreatedAt: current.Movie.CreatedAt.UnixMilli(),
Creator: op.GetUserName(current.Movie.CreatorID), Base: current.Movie.Base,
Creator: op.GetUserName(current.Movie.CreatorID),
CreatorId: current.Movie.CreatorID,
}, },
} }
if c.Movie.Base.Type == "" && c.Movie.Base.Url != "" {
c.Movie.Base.Type = utils.GetUrlExtension(c.Movie.Base.Url)
}
// hide url and headers when proxy
if c.Movie.Base.RtmpSource || c.Movie.Base.Live && c.Movie.Base.Proxy {
t := c.Movie.Base.Type
if t != "flv" && t != "m3u8" {
t = "m3u8"
}
c.Movie.Base.Url = fmt.Sprintf("/api/movie/live/%s.%s", current.Movie.ID, t)
c.Movie.Base.Headers = nil
} else if c.Movie.Base.Proxy {
c.Movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s", current.Movie.RoomID, current.Movie.ID)
c.Movie.Base.Headers = nil
}
return c return c
} }
@ -129,7 +131,8 @@ func CurrentMovie(ctx *gin.Context) {
room := ctx.MustGet("room").(*op.Room) room := ctx.MustGet("room").(*op.Room)
user := ctx.MustGet("user").(*op.User) user := ctx.MustGet("user").(*op.User)
current, err := genCurrent(ctx, user, room, room.Current()) current := room.Current()
err := genCurrent(ctx, user, room, current)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return return
@ -538,6 +541,7 @@ func proxyURL(ctx *gin.Context, u string, headers map[string]string) error {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
req.Header.Set("Range", ctx.GetHeader("Range")) req.Header.Set("Range", ctx.GetHeader("Range"))
req.Header.Set("Accept-Encoding", ctx.GetHeader("Accept-Encoding"))
if req.Header.Get("User-Agent") == "" { if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", utils.UA) req.Header.Set("User-Agent", utils.UA)
} }
@ -546,11 +550,11 @@ func proxyURL(ctx *gin.Context, u string, headers map[string]string) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
ctx.Header("Content-Type", resp.Header.Get("Content-Type"))
ctx.Header("Content-Length", resp.Header.Get("Content-Length"))
ctx.Header("Accept-Ranges", resp.Header.Get("Accept-Ranges")) ctx.Header("Accept-Ranges", resp.Header.Get("Accept-Ranges"))
ctx.Header("Cache-Control", resp.Header.Get("Cache-Control")) ctx.Header("Cache-Control", resp.Header.Get("Cache-Control"))
ctx.Header("Content-Length", resp.Header.Get("Content-Length"))
ctx.Header("Content-Range", resp.Header.Get("Content-Range")) ctx.Header("Content-Range", resp.Header.Get("Content-Range"))
ctx.Header("Content-Type", resp.Header.Get("Content-Type"))
ctx.Status(resp.StatusCode) ctx.Status(resp.StatusCode)
io.Copy(ctx.Writer, resp.Body) io.Copy(ctx.Writer, resp.Body)
return nil return nil
@ -731,6 +735,85 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
} }
} }
case dbModel.VendorAlist:
case dbModel.VendorEmby:
t := ctx.Query("t")
switch t {
case "":
if !movie.Movie.Base.Proxy {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
return
}
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
embyC, err := movie.EmbyCache().Get(ctx, u.EmbyCache())
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
source, err := strconv.Atoi(ctx.Query("source"))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if source >= len(embyC.Sources) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("source out of range"))
return
}
id, err := strconv.Atoi(ctx.Query("id"))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if id >= len(embyC.Sources[source].URLs) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
return
}
proxyURL(ctx, embyC.Sources[source].URLs[id].URL, nil)
return
case "subtitle":
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
embyC, err := movie.EmbyCache().Get(ctx, u.EmbyCache())
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
source, err := strconv.Atoi(ctx.Query("source"))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if source >= len(embyC.Sources) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("source out of range"))
return
}
id, err := strconv.Atoi(ctx.Query("id"))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if id >= len(embyC.Sources[source].Subtitles) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
return
}
data, err := embyC.Sources[source].Subtitles[id].Cache.Get(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
ctx.Data(http.StatusOK, "text/plain; charset=utf-8", data)
return
}
default: default:
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support proxy")) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support proxy"))
return return
@ -814,32 +897,78 @@ func parse2VendorMovie(ctx context.Context, user *op.User, room *op.Room, movie
return err return err
} }
// TODO: when proxy if !movie.Base.Proxy {
for i, es := range data.Sources { for i, es := range data.Sources {
if len(es.URLs) == 0 { if len(es.URLs) == 0 {
if i != len(data.Sources)-1 { 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 continue
} }
if movie.Base.Url == "" { for _, s := range es.Subtitles {
return errors.New("no source") 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: s.Type,
}
} }
} }
movie.Base.Url = es.URLs[0].URL } else {
for si, es := range data.Sources {
if len(es.URLs) == 0 {
if si != len(data.Sources)-1 {
continue
}
if movie.Base.Url == "" {
return errors.New("no source")
}
}
if len(es.Subtitles) == 0 { rawPath, err := url.JoinPath("/api/movie/proxy", movie.RoomID, movie.ID)
continue if err != nil {
} return err
for _, s := range es.Subtitles { }
if movie.Base.Subtitles == nil { rawQuery := url.Values{}
movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles)) rawQuery.Set("source", strconv.Itoa(si))
rawQuery.Set("id", strconv.Itoa(0))
u := url.URL{
Path: rawPath,
RawQuery: rawQuery.Encode(),
}
movie.Base.Url = u.String()
if len(es.Subtitles) == 0 {
continue
} }
movie.Base.Subtitles[s.Name] = &dbModel.Subtitle{ for sbi, s := range es.Subtitles {
URL: s.URL, if movie.Base.Subtitles == nil {
Type: "srt", movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles))
}
rawQuery := url.Values{}
rawQuery.Set("t", "subtitle")
rawQuery.Set("source", strconv.Itoa(si))
rawQuery.Set("id", strconv.Itoa(sbi))
u := url.URL{
Path: rawPath,
RawQuery: rawQuery.Encode(),
}
movie.Base.Subtitles[s.Name] = &dbModel.Subtitle{
URL: u.String(),
Type: s.Type,
}
} }
} }
} }
movie.Base.VendorInfo.Emby = nil
return nil return nil
default: default:

@ -145,9 +145,11 @@ func (s *SwapMovieReq) Validate() error {
} }
type MoviesResp struct { type MoviesResp struct {
Id string `json:"id"` Id string `json:"id"`
Base model.BaseMovie `json:"base"` CreatedAt int64 `json:"createAt"`
Creator string `json:"creator"` Base model.BaseMovie `json:"base"`
Creator string `json:"creator"`
CreatorId string `json:"creatorId"`
} }
type CurrentMovieResp struct { type CurrentMovieResp struct {

Loading…
Cancel
Save