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/alist.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)
}