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.
436 lines
9.4 KiB
Go
436 lines
9.4 KiB
Go
package cache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"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/alist"
|
|
"github.com/zijiren233/gencontainer/refreshcache0"
|
|
"github.com/zijiren233/gencontainer/refreshcache1"
|
|
"github.com/zijiren233/go-uhc"
|
|
)
|
|
|
|
type AlistUserCache = MapCache[*AlistUserCacheData, struct{}]
|
|
|
|
type AlistUserCacheData struct {
|
|
Host string
|
|
ServerID string
|
|
Token string
|
|
Backend string
|
|
}
|
|
|
|
func NewAlistUserCache(userID string) *AlistUserCache {
|
|
return newMapCache(
|
|
func(ctx context.Context, key string, _ ...struct{}) (*AlistUserCacheData, error) {
|
|
return AlistAuthorizationCacheWithUserIDInitFunc(ctx, userID, key)
|
|
},
|
|
-1,
|
|
)
|
|
}
|
|
|
|
func AlistAuthorizationCacheWithUserIDInitFunc(
|
|
ctx context.Context,
|
|
userID, serverID string,
|
|
) (*AlistUserCacheData, error) {
|
|
v, err := db.GetAlistVendor(userID, serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return AlistAuthorizationCacheWithConfigInitFunc(ctx, v)
|
|
}
|
|
|
|
func AlistAuthorizationCacheWithConfigInitFunc(
|
|
ctx context.Context,
|
|
v *model.AlistVendor,
|
|
) (*AlistUserCacheData, error) {
|
|
cli := vendor.LoadAlistClient(v.Backend)
|
|
model.GenAlistServerID(v)
|
|
|
|
if v.Username == "" {
|
|
_, err := cli.Me(ctx, &alist.MeReq{
|
|
Host: v.Host,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &AlistUserCacheData{
|
|
Host: v.Host,
|
|
ServerID: v.ServerID,
|
|
Backend: v.Backend,
|
|
}, nil
|
|
}
|
|
|
|
resp, err := cli.Login(ctx, &alist.LoginReq{
|
|
Host: v.Host,
|
|
Username: v.Username,
|
|
Password: string(v.HashedPassword),
|
|
Hashed: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &AlistUserCacheData{
|
|
Host: v.Host,
|
|
ServerID: v.ServerID,
|
|
Token: resp.GetToken(),
|
|
Backend: v.Backend,
|
|
}, nil
|
|
}
|
|
|
|
type AlistMovieCache = refreshcache1.RefreshCache[*AlistMovieCacheData, *AlistMovieCacheFuncArgs]
|
|
|
|
func NewAlistMovieCache(movie *model.Movie, subPath string) *AlistMovieCache {
|
|
return refreshcache1.NewRefreshCache(NewAlistMovieCacheInitFunc(movie, subPath), -1)
|
|
}
|
|
|
|
type AlistProvider = string
|
|
|
|
const (
|
|
AlistProviderAli = "AliyundriveOpen"
|
|
AlistProvider115 = "115 Cloud"
|
|
)
|
|
|
|
type AlistSubtitle struct {
|
|
Cache *SubtitleDataCache
|
|
Name string
|
|
URL string
|
|
Type string
|
|
}
|
|
|
|
type AlistMovieCacheData struct {
|
|
Ali *refreshcache0.RefreshCache[*AlistAliCache]
|
|
URL string
|
|
Provider string
|
|
Subtitles []*AlistSubtitle
|
|
}
|
|
|
|
type AlistAliCache struct {
|
|
URL string
|
|
M3U8ListFile []byte
|
|
Subtitles []*AlistSubtitle
|
|
}
|
|
|
|
type SubtitleDataCache = refreshcache0.RefreshCache[[]byte]
|
|
|
|
const subtitleMaxLength = 15 * 1024 * 1024
|
|
|
|
func newAliSubtitles(
|
|
list []*alist.FsOtherResp_VideoPreviewPlayInfo_LiveTranscodingSubtitleTaskList,
|
|
) []*AlistSubtitle {
|
|
caches := make([]*AlistSubtitle, len(list))
|
|
for i, v := range list {
|
|
if v.GetStatus() != "finished" {
|
|
return nil
|
|
}
|
|
|
|
url := v.GetUrl()
|
|
caches[i] = &AlistSubtitle{
|
|
Cache: refreshcache0.NewRefreshCache(func(ctx context.Context) ([]byte, error) {
|
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := uhc.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)
|
|
}
|
|
|
|
if resp.ContentLength > subtitleMaxLength {
|
|
return nil, fmt.Errorf(
|
|
"subtitle too large, got: %d, max: %d",
|
|
resp.ContentLength,
|
|
subtitleMaxLength,
|
|
)
|
|
}
|
|
|
|
return io.ReadAll(io.LimitReader(resp.Body, subtitleMaxLength))
|
|
}, -1),
|
|
Name: v.GetLanguage(),
|
|
URL: v.GetUrl(),
|
|
Type: utils.GetFileExtension(v.GetUrl()),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if v.GetStatus() != "finished" {
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(
|
|
buf,
|
|
"#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d,NAME=\"%d\"\n",
|
|
v.GetTemplateWidth()*v.GetTemplateHeight(),
|
|
v.GetTemplateWidth(),
|
|
v.GetTemplateHeight(),
|
|
v.GetTemplateWidth(),
|
|
)
|
|
buf.WriteString(v.GetUrl() + "\n")
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
type AlistMovieCacheFuncArgs struct {
|
|
UserCache *AlistUserCache
|
|
UserAgent string
|
|
}
|
|
|
|
func NewAlistMovieCacheInitFunc(
|
|
movie *model.Movie,
|
|
subPath string,
|
|
) func(ctx context.Context, args *AlistMovieCacheFuncArgs) (*AlistMovieCacheData, error) {
|
|
return func(ctx context.Context, args *AlistMovieCacheFuncArgs) (*AlistMovieCacheData, error) {
|
|
if err := validateArgs(args, movie, subPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serverID, truePath, err := getServerIDAndPath(movie, subPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aucd, err := args.UserCache.LoadOrStore(ctx, serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if aucd.Host == "" {
|
|
return nil, errors.New("not bind alist vendor")
|
|
}
|
|
|
|
cli := vendor.LoadAlistClient(movie.VendorInfo.Backend)
|
|
|
|
fg, err := getFsGet(
|
|
ctx,
|
|
cli,
|
|
aucd,
|
|
truePath,
|
|
movie.VendorInfo.Alist.Password,
|
|
args.UserAgent,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fg.GetIsDir() {
|
|
return nil, fmt.Errorf("path is dir: %s", truePath)
|
|
}
|
|
|
|
cache := &AlistMovieCacheData{
|
|
URL: fg.GetRawUrl(),
|
|
Provider: fg.GetProvider(),
|
|
}
|
|
|
|
if err := processSubtitles(ctx, cli, aucd, fg, truePath, movie.VendorInfo.Alist.Password, args.UserAgent, cache); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fg.GetProvider() == AlistProviderAli {
|
|
processAliProvider(
|
|
ctx,
|
|
fg.GetRawUrl(),
|
|
cli,
|
|
aucd,
|
|
truePath,
|
|
movie.VendorInfo.Alist.Password,
|
|
cache,
|
|
)
|
|
}
|
|
|
|
return cache, nil
|
|
}
|
|
}
|
|
|
|
func validateArgs(args *AlistMovieCacheFuncArgs, movie *model.Movie, subPath string) error {
|
|
if args == nil {
|
|
return errors.New("need alist user cache")
|
|
}
|
|
|
|
if args.UserCache == nil {
|
|
return errors.New("need alist user cache")
|
|
}
|
|
|
|
if movie.IsFolder && subPath == "" {
|
|
return errors.New("sub path is empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getServerIDAndPath(movie *model.Movie, subPath string) (string, string, error) {
|
|
serverID, truePath, err := movie.VendorInfo.Alist.ServerIDAndFilePath()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if movie.IsFolder {
|
|
newPath := path.Join(truePath, subPath)
|
|
if !strings.HasPrefix(newPath, truePath) {
|
|
return "", "", errors.New("sub path is not in parent path")
|
|
}
|
|
|
|
truePath = newPath
|
|
}
|
|
|
|
return serverID, truePath, nil
|
|
}
|
|
|
|
func getFsGet(
|
|
ctx context.Context,
|
|
cli alist.AlistHTTPServer,
|
|
aucd *AlistUserCacheData,
|
|
truePath, password, userAgent string,
|
|
) (*alist.FsGetResp, error) {
|
|
return cli.FsGet(ctx, &alist.FsGetReq{
|
|
Host: aucd.Host,
|
|
Token: aucd.Token,
|
|
Path: truePath,
|
|
Password: password,
|
|
UserAgent: userAgent,
|
|
})
|
|
}
|
|
|
|
func processSubtitles(
|
|
ctx context.Context,
|
|
cli alist.AlistHTTPServer,
|
|
aucd *AlistUserCacheData,
|
|
fg *alist.FsGetResp,
|
|
truePath, password, userAgent string,
|
|
cache *AlistMovieCacheData,
|
|
) error {
|
|
prefix := strings.TrimSuffix(truePath, fg.GetName())
|
|
for _, related := range fg.GetRelated() {
|
|
if related.GetType() != 4 {
|
|
continue
|
|
}
|
|
|
|
if utils.GetFileExtension(related.GetName()) == "xml" {
|
|
continue
|
|
}
|
|
|
|
resp, err := getFsGet(ctx, cli, aucd, prefix+related.GetName(), password, userAgent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subtitle := &AlistSubtitle{
|
|
Name: related.GetName(),
|
|
URL: resp.GetRawUrl(),
|
|
Type: utils.GetFileExtension(resp.GetName()),
|
|
Cache: refreshcache0.NewRefreshCache(func(ctx context.Context) ([]byte, error) {
|
|
return fetchSubtitleContent(ctx, resp.GetRawUrl())
|
|
}, -1),
|
|
}
|
|
cache.Subtitles = append(cache.Subtitles, subtitle)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func fetchSubtitleContent(ctx context.Context, url string) ([]byte, error) {
|
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := uhc.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)
|
|
}
|
|
|
|
if resp.ContentLength > subtitleMaxLength {
|
|
return nil, fmt.Errorf(
|
|
"subtitle too large, got: %d, max: %d",
|
|
resp.ContentLength,
|
|
subtitleMaxLength,
|
|
)
|
|
}
|
|
|
|
return io.ReadAll(io.LimitReader(resp.Body, subtitleMaxLength))
|
|
}
|
|
|
|
func processAliProvider(
|
|
_ context.Context,
|
|
firstURL string,
|
|
cli alist.AlistHTTPServer,
|
|
aucd *AlistUserCacheData,
|
|
truePath, password string,
|
|
cache *AlistMovieCacheData,
|
|
) {
|
|
cache.Ali = refreshcache0.NewRefreshCache(func(ctx context.Context) (*AlistAliCache, error) {
|
|
var url string
|
|
if firstURL != "" {
|
|
url = firstURL
|
|
firstURL = ""
|
|
} else {
|
|
u, err := cli.FsGet(ctx, &alist.FsGetReq{
|
|
Host: aucd.Host,
|
|
Token: aucd.Token,
|
|
Path: truePath,
|
|
Password: password,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url = u.GetRawUrl()
|
|
}
|
|
|
|
fo, err := cli.FsOther(ctx, &alist.FsOtherReq{
|
|
Host: aucd.Host,
|
|
Token: aucd.Token,
|
|
Path: truePath,
|
|
Password: password,
|
|
Method: "video_preview",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &AlistAliCache{
|
|
URL: url,
|
|
M3U8ListFile: genAliM3U8ListFile(
|
|
fo.GetVideoPreviewPlayInfo().GetLiveTranscodingTaskList(),
|
|
),
|
|
Subtitles: newAliSubtitles(
|
|
fo.GetVideoPreviewPlayInfo().GetLiveTranscodingSubtitleTaskList(),
|
|
),
|
|
}, nil
|
|
}, 14*time.Minute)
|
|
}
|