diff --git a/internal/cache/alist.go b/internal/cache/alist.go index 0042668..6969288 100644 --- a/internal/cache/alist.go +++ b/internal/cache/alist.go @@ -1,8 +1,12 @@ package cache import ( + "bytes" "context" "errors" + "fmt" + "io" + "net/http" "time" "github.com/synctv-org/synctv/internal/db" @@ -73,14 +77,57 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali type AlistMovieCache = refreshcache.RefreshCache[*AlistMovieCacheData, *AlistUserCache] func NewAlistMovieCache(movie *model.Movie) *AlistMovieCache { - return refreshcache.NewRefreshCache(NewAlistMovieCacheInitFunc(movie), time.Hour) + return refreshcache.NewRefreshCache(NewAlistMovieCacheInitFunc(movie), time.Minute*15) } type AlistMovieCacheData struct { - URLs []struct { - URL string - Name string + URL string + AliM3U8ListFile []byte + AliSubtitles []*AliSubtitle +} + +type AliSubtitle struct { + Raw *alist.FsOtherResp_VideoPreviewPlayInfo_LiveTranscodingSubtitleTaskList + Cache *AliSubtitleCache +} + +type AliSubtitleCache = refreshcache.RefreshCache[[]byte, struct{}] + +func newAliSubtitlesCacheInitFunc(list []*alist.FsOtherResp_VideoPreviewPlayInfo_LiveTranscodingSubtitleTaskList) []*AliSubtitle { + caches := make([]*AliSubtitle, len(list)) + for i, v := range list { + url := v.Url + caches[i] = &AliSubtitle{ + Cache: refreshcache.NewRefreshCache(func(ctx context.Context, args ...struct{}) ([]byte, error) { + r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code: %d", resp.StatusCode) + } + return io.ReadAll(resp.Body) + }, 0), + Raw: v, + } } + return caches +} + +func genAliM3U8ListFile(urls []*alist.FsOtherResp_VideoPreviewPlayInfo_LiveTranscodingTaskList) []byte { + buf := bytes.NewBuffer(nil) + buf.WriteString("#EXTM3U\n") + buf.WriteString("#EXT-X-VERSION:3\n") + for _, v := range urls { + buf.WriteString(fmt.Sprintf("#EXT-X-STREAM-INF:RESOLUTION=%dx%d,NAME=\"%d\"\n", v.TemplateWidth, v.TemplateHeight, v.TemplateWidth)) + buf.WriteString(v.Url + "\n") + } + return buf.Bytes() } func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, args ...*AlistUserCache) (*AlistMovieCacheData, error) { @@ -111,14 +158,7 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar } cache := &AlistMovieCacheData{ - URLs: []struct { - URL string - Name string - }{ - { - URL: fg.RawUrl, - }, - }, + URL: fg.RawUrl, } if fg.Provider == "AliyundriveOpen" { fo, err := cli.FsOther(ctx, &alist.FsOtherReq{ @@ -131,19 +171,8 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar if err != nil { return nil, err } - cache.URLs = make([]struct { - URL string - Name string - }, len(fo.VideoPreviewPlayInfo.LiveTranscodingTaskList)) - for i, v := range fo.VideoPreviewPlayInfo.LiveTranscodingTaskList { - cache.URLs[i] = struct { - URL string - Name string - }{ - URL: v.Url, - Name: v.TemplateId, - } - } + cache.AliM3U8ListFile = genAliM3U8ListFile(fo.VideoPreviewPlayInfo.LiveTranscodingTaskList) + cache.AliSubtitles = newAliSubtitlesCacheInitFunc(fo.VideoPreviewPlayInfo.LiveTranscodingSubtitleTaskList) } return cache, nil } diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 9e89f37..8cadc17 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -556,7 +556,10 @@ func proxyURL(ctx *gin.Context, u string, headers map[string]string) error { ctx.Header("Content-Range", resp.Header.Get("Content-Range")) ctx.Header("Content-Type", resp.Header.Get("Content-Type")) ctx.Status(resp.StatusCode) - io.Copy(ctx.Writer, resp.Body) + _, err = io.Copy(ctx.Writer, resp.Body) + if err != nil && err != io.EOF { + return err + } return nil } @@ -736,10 +739,6 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { } case dbModel.VendorAlist: - 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)) @@ -750,20 +749,41 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return } - idS := ctx.Query("id") - if idS == "" { - idS = "0" - } - id, err := strconv.Atoi(idS) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - if id >= len(alistC.URLs) { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range")) + if len(alistC.AliM3U8ListFile) != 0 { + t := ctx.Query("t") + switch t { + case "": + ctx.Data(http.StatusOK, "audio/mpegurl", alistC.AliM3U8ListFile) + return + case "subtitle": + idS := ctx.Query("id") + if idS == "" { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id is empty")) + return + } + id, err := strconv.Atoi(idS) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + if id >= len(alistC.AliSubtitles) { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range")) + return + } + data, err := alistC.AliSubtitles[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) + } + } else if !movie.Movie.Base.Proxy { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy")) return + } else { + proxyURL(ctx, alistC.URL, nil) } - proxyURL(ctx, alistC.URLs[id].URL, nil) + return case dbModel.VendorEmby: @@ -908,25 +928,38 @@ func parse2VendorMovie(ctx context.Context, user *op.User, room *op.Room, movie return err } - if len(data.URLs) == 0 { - return errors.New("no source") - } - id := len(data.URLs) - 1 - if !movie.Base.Proxy { - movie.Base.Url = data.URLs[id].URL + if len(data.AliM3U8ListFile) != 0 { + rawPath, err := url.JoinPath("/api/movie/proxy", movie.RoomID, movie.ID) + if err != nil { + return err + } + u := url.URL{ + Path: rawPath, + } + movie.Base.Url = u.String() + movie.Base.Type = "m3u8" + + for i, s := range data.AliSubtitles { + if movie.Base.Subtitles == nil { + movie.Base.Subtitles = make(map[string]*dbModel.Subtitle, len(data.AliSubtitles)) + } + movie.Base.Subtitles[s.Raw.Language] = &dbModel.Subtitle{ + URL: fmt.Sprintf("/api/movie/proxy/%s/%s?t=subtitle&id=%d", movie.RoomID, movie.ID, i), + Type: utils.GetUrlExtension(s.Raw.Url), + } + } + } else if !movie.Base.Proxy { + movie.Base.Url = data.URL } else { rawPath, err := url.JoinPath("/api/movie/proxy", movie.RoomID, movie.ID) if err != nil { return err } - rawQuery := url.Values{} - rawQuery.Set("id", strconv.Itoa(id)) u := url.URL{ - Path: rawPath, - RawQuery: rawQuery.Encode(), + Path: rawPath, } movie.Base.Url = u.String() - movie.Base.Type = utils.GetUrlExtension(data.URLs[id].URL) + movie.Base.Type = utils.GetUrlExtension(data.URL) } movie.Base.VendorInfo.Alist.Password = "" return nil