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.
432 lines
12 KiB
Go
432 lines
12 KiB
Go
package cache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"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/bilibili"
|
|
"github.com/zencoder/go-dash/v3/mpd"
|
|
"github.com/zijiren233/gencontainer/refreshcache"
|
|
"github.com/zijiren233/gencontainer/refreshcache0"
|
|
"github.com/zijiren233/gencontainer/refreshcache1"
|
|
"github.com/zijiren233/go-uhc"
|
|
)
|
|
|
|
type BilibiliMpdCache struct {
|
|
Mpd *mpd.MPD
|
|
HevcMpd *mpd.MPD
|
|
URLs []string
|
|
}
|
|
|
|
type BilibiliSubtitleCache map[string]*BilibiliSubtitleCacheItem
|
|
|
|
type BilibiliSubtitleCacheItem struct {
|
|
Srt *refreshcache0.RefreshCache[[]byte]
|
|
URL string
|
|
}
|
|
|
|
func NewBilibiliSharedMpdCacheInitFunc(movie *model.Movie) func(ctx context.Context, args *BilibiliUserCache) (*BilibiliMpdCache, error) {
|
|
return func(ctx context.Context, args *BilibiliUserCache) (*BilibiliMpdCache, error) {
|
|
return BilibiliSharedMpdCacheInitFunc(ctx, movie, args)
|
|
}
|
|
}
|
|
|
|
func BilibiliSharedMpdCacheInitFunc(ctx context.Context, movie *model.Movie, args *BilibiliUserCache) (*BilibiliMpdCache, error) {
|
|
if args == nil {
|
|
return nil, errors.New("no bilibili user cache data")
|
|
}
|
|
|
|
cookies, err := getBilibiliCookies(ctx, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
|
|
m, hevcM, err := getBilibiliMpd(ctx, cli, movie.MovieBase.VendorInfo.Bilibili, cookies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.BaseURL = append(m.BaseURL, "/api/room/movie/proxy/")
|
|
movies := processMpdUrls(m, hevcM, movie.ID, movie.RoomID)
|
|
|
|
return &BilibiliMpdCache{
|
|
URLs: movies,
|
|
Mpd: m,
|
|
HevcMpd: hevcM,
|
|
}, nil
|
|
}
|
|
|
|
func getBilibiliCookies(ctx context.Context, args *BilibiliUserCache) ([]*http.Cookie, error) {
|
|
vendorInfo, err := args.Get(ctx)
|
|
if err != nil {
|
|
if !errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
return vendorInfo.Cookies, nil
|
|
}
|
|
|
|
func getBilibiliMpd(ctx context.Context, cli bilibili.BilibiliHTTPServer, biliInfo *model.BilibiliStreamingInfo, cookies []*http.Cookie) (*mpd.MPD, *mpd.MPD, error) {
|
|
cookiesMap := utils.HTTPCookieToMap(cookies)
|
|
|
|
switch {
|
|
case biliInfo.Epid != 0:
|
|
resp, err := cli.GetDashPGCURL(ctx, &bilibili.GetDashPGCURLReq{
|
|
Cookies: cookiesMap,
|
|
Epid: biliInfo.Epid,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return parseMpdResponse(resp.Mpd, resp.HevcMpd)
|
|
|
|
case biliInfo.Bvid != "":
|
|
resp, err := cli.GetDashVideoURL(ctx, &bilibili.GetDashVideoURLReq{
|
|
Cookies: cookiesMap,
|
|
Bvid: biliInfo.Bvid,
|
|
Cid: biliInfo.Cid,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return parseMpdResponse(resp.Mpd, resp.HevcMpd)
|
|
|
|
default:
|
|
return nil, nil, errors.New("bvid and epid are empty")
|
|
}
|
|
}
|
|
|
|
func parseMpdResponse(mpdStr, hevcMpdStr string) (*mpd.MPD, *mpd.MPD, error) {
|
|
m, err := mpd.ReadFromString(mpdStr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
hevcM, err := mpd.ReadFromString(hevcMpdStr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return m, hevcM, nil
|
|
}
|
|
|
|
func processMpdUrls(m, hevcM *mpd.MPD, movieID, roomID string) []string {
|
|
movies := []string{}
|
|
id := 0
|
|
|
|
// Process regular MPD
|
|
for _, p := range m.Periods {
|
|
for _, as := range p.AdaptationSets {
|
|
for _, r := range as.Representations {
|
|
for i := range r.BaseURL {
|
|
movies = append(movies, r.BaseURL[i])
|
|
r.BaseURL[i] = fmt.Sprintf("%s?id=%d&roomId=%s", movieID, id, roomID)
|
|
id++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process HEVC MPD
|
|
for _, p := range hevcM.Periods {
|
|
for _, as := range p.AdaptationSets {
|
|
for _, r := range as.Representations {
|
|
for i := range r.BaseURL {
|
|
movies = append(movies, r.BaseURL[i])
|
|
r.BaseURL[i] = fmt.Sprintf("%s?id=%d&roomId=%s&t=hevc", movieID, id, roomID)
|
|
id++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return movies
|
|
}
|
|
|
|
func BilibiliMpdToString(mpdRaw *mpd.MPD, token string) (string, error) {
|
|
newMpdRaw := *mpdRaw
|
|
newPeriods := make([]*mpd.Period, len(mpdRaw.Periods))
|
|
for i, p := range mpdRaw.Periods {
|
|
n := *p
|
|
newPeriods[i] = &n
|
|
}
|
|
newMpdRaw.Periods = newPeriods
|
|
for _, p := range newMpdRaw.Periods {
|
|
newAdaptationSets := make([]*mpd.AdaptationSet, len(p.AdaptationSets))
|
|
for i, as := range p.AdaptationSets {
|
|
n := *as
|
|
newAdaptationSets[i] = &n
|
|
}
|
|
p.AdaptationSets = newAdaptationSets
|
|
for _, as := range p.AdaptationSets {
|
|
newRepresentations := make([]*mpd.Representation, len(as.Representations))
|
|
for i, r := range as.Representations {
|
|
n := *r
|
|
newRepresentations[i] = &n
|
|
}
|
|
as.Representations = newRepresentations
|
|
for _, r := range as.Representations {
|
|
newBaseURL := make([]string, len(r.BaseURL))
|
|
copy(newBaseURL, r.BaseURL)
|
|
r.BaseURL = newBaseURL
|
|
for i := range r.BaseURL {
|
|
r.BaseURL[i] = fmt.Sprintf("%s&token=%s", r.BaseURL[i], token)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newMpdRaw.WriteToString()
|
|
}
|
|
|
|
func NewBilibiliNoSharedMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, key string, args ...*BilibiliUserCache) (string, error) {
|
|
return func(ctx context.Context, key string, args ...*BilibiliUserCache) (string, error) {
|
|
return BilibiliNoSharedMovieCacheInitFunc(ctx, movie, args...)
|
|
}
|
|
}
|
|
|
|
func BilibiliNoSharedMovieCacheInitFunc(ctx context.Context, movie *model.Movie, args ...*BilibiliUserCache) (string, error) {
|
|
if len(args) == 0 {
|
|
return "", errors.New("no bilibili user cache data")
|
|
}
|
|
var cookies []*http.Cookie
|
|
vendorInfo, err := args[0].Get(ctx)
|
|
if err != nil {
|
|
if !errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
|
return "", err
|
|
}
|
|
} else {
|
|
cookies = vendorInfo.Cookies
|
|
}
|
|
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
|
|
var u string
|
|
biliInfo := movie.MovieBase.VendorInfo.Bilibili
|
|
switch {
|
|
case biliInfo.Epid != 0:
|
|
resp, err := cli.GetPGCURL(ctx, &bilibili.GetPGCURLReq{
|
|
Cookies: utils.HTTPCookieToMap(cookies),
|
|
Epid: biliInfo.Epid,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
u = resp.Url
|
|
|
|
case biliInfo.Bvid != "":
|
|
resp, err := cli.GetVideoURL(ctx, &bilibili.GetVideoURLReq{
|
|
Cookies: utils.HTTPCookieToMap(cookies),
|
|
Bvid: biliInfo.Bvid,
|
|
Cid: biliInfo.Cid,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
u = resp.Url
|
|
|
|
default:
|
|
return "", errors.New("bvid and epid are empty")
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
//nolint:tagliatelle
|
|
type bilibiliSubtitleResp struct {
|
|
FontColor string `json:"font_color"`
|
|
BackgroundColor string `json:"background_color"`
|
|
Stroke string `json:"Stroke"`
|
|
Type string `json:"type"`
|
|
Lang string `json:"lang"`
|
|
Version string `json:"version"`
|
|
Body []struct {
|
|
Content string `json:"content"`
|
|
From float64 `json:"from"`
|
|
To float64 `json:"to"`
|
|
Sid int `json:"sid"`
|
|
Location int `json:"location"`
|
|
} `json:"body"`
|
|
FontSize float64 `json:"font_size"`
|
|
BackgroundAlpha float64 `json:"background_alpha"`
|
|
}
|
|
|
|
func NewBilibiliSubtitleCacheInitFunc(movie *model.Movie) func(ctx context.Context, args *BilibiliUserCache) (BilibiliSubtitleCache, error) {
|
|
return func(ctx context.Context, args *BilibiliUserCache) (BilibiliSubtitleCache, error) {
|
|
return BilibiliSubtitleCacheInitFunc(ctx, movie, args)
|
|
}
|
|
}
|
|
|
|
func BilibiliSubtitleCacheInitFunc(ctx context.Context, movie *model.Movie, args *BilibiliUserCache) (BilibiliSubtitleCache, error) {
|
|
if args == nil {
|
|
return nil, errors.New("no bilibili user cache data")
|
|
}
|
|
|
|
biliInfo := movie.MovieBase.VendorInfo.Bilibili
|
|
if biliInfo.Bvid == "" || biliInfo.Cid == 0 {
|
|
return nil, errors.New("bvid or cid is empty")
|
|
}
|
|
|
|
// must login
|
|
var cookies []*http.Cookie
|
|
vendorInfo, err := args.Get(ctx)
|
|
if err != nil {
|
|
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
|
return make(BilibiliSubtitleCache), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
cookies = vendorInfo.Cookies
|
|
|
|
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
|
|
resp, err := cli.GetSubtitles(ctx, &bilibili.GetSubtitlesReq{
|
|
Cookies: utils.HTTPCookieToMap(cookies),
|
|
Bvid: biliInfo.Bvid,
|
|
Cid: biliInfo.Cid,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
subtitleCache := make(BilibiliSubtitleCache, len(resp.Subtitles))
|
|
for k, v := range resp.Subtitles {
|
|
subtitleCache[k] = &BilibiliSubtitleCacheItem{
|
|
URL: v,
|
|
Srt: refreshcache0.NewRefreshCache[[]byte](func(ctx context.Context) ([]byte, error) {
|
|
return translateBilibiliSubtitleToSrt(ctx, v)
|
|
}, 0),
|
|
}
|
|
}
|
|
|
|
return subtitleCache, nil
|
|
}
|
|
|
|
func convertToSRT(subtitles *bilibiliSubtitleResp) []byte {
|
|
srt := bytes.NewBuffer(nil)
|
|
counter := 0
|
|
for _, subtitle := range subtitles.Body {
|
|
srt.WriteString(
|
|
fmt.Sprintf("%d\n%s --> %s\n%s\n\n",
|
|
counter,
|
|
formatTime(subtitle.From),
|
|
formatTime(subtitle.To),
|
|
subtitle.Content))
|
|
counter++
|
|
}
|
|
return srt.Bytes()
|
|
}
|
|
|
|
func formatTime(seconds float64) string {
|
|
hours := int(seconds) / 3600
|
|
seconds = math.Mod(seconds, 3600)
|
|
minutes := int(seconds) / 60
|
|
seconds = math.Mod(seconds, 60)
|
|
milliseconds := int((seconds - float64(int(seconds))) * 1000)
|
|
return fmt.Sprintf("%02d:%02d:%02d,%03d", hours, minutes, int(seconds), milliseconds)
|
|
}
|
|
|
|
func translateBilibiliSubtitleToSrt(ctx context.Context, url string) ([]byte, error) {
|
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, "https:"+url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Header.Set("User-Agent", utils.UA)
|
|
r.Header.Set("Referer", "https://www.bilibili.com")
|
|
resp, err := uhc.Do(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
var srt bilibiliSubtitleResp
|
|
err = json.NewDecoder(resp.Body).Decode(&srt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertToSRT(&srt), nil
|
|
}
|
|
|
|
type BilibiliLiveCache struct{}
|
|
|
|
func NewBilibiliLiveCacheInitFunc(movie *model.Movie) func(ctx context.Context) ([]byte, error) {
|
|
return func(ctx context.Context) ([]byte, error) {
|
|
return BilibiliLiveCacheInitFunc(ctx, movie)
|
|
}
|
|
}
|
|
|
|
func genBilibiliLiveM3U8ListFile(urls []*bilibili.LiveStream) []byte {
|
|
buf := bytes.NewBuffer(nil)
|
|
buf.WriteString("#EXTM3U\n")
|
|
buf.WriteString("#EXT-X-VERSION:3\n")
|
|
for _, v := range urls {
|
|
if len(v.Urls) == 0 {
|
|
continue
|
|
}
|
|
buf.WriteString(fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,NAME=\"%s\"\n", 1920*1080*v.Quality, v.Desc))
|
|
buf.WriteString(v.Urls[0] + "\n")
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func BilibiliLiveCacheInitFunc(ctx context.Context, movie *model.Movie) ([]byte, error) {
|
|
cli := vendor.LoadBilibiliClient(movie.MovieBase.VendorInfo.Backend)
|
|
resp, err := cli.GetLiveStreams(ctx, &bilibili.GetLiveStreamsReq{
|
|
Cid: movie.MovieBase.VendorInfo.Bilibili.Cid,
|
|
Hls: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return genBilibiliLiveM3U8ListFile(resp.LiveStreams), nil
|
|
}
|
|
|
|
type BilibiliMovieCache struct {
|
|
NoSharedMovie *MapCache[string, *BilibiliUserCache]
|
|
SharedMpd *refreshcache1.RefreshCache[*BilibiliMpdCache, *BilibiliUserCache]
|
|
Subtitle *refreshcache1.RefreshCache[BilibiliSubtitleCache, *BilibiliUserCache]
|
|
Live *refreshcache0.RefreshCache[[]byte]
|
|
}
|
|
|
|
func NewBilibiliMovieCache(movie *model.Movie) *BilibiliMovieCache {
|
|
return &BilibiliMovieCache{
|
|
NoSharedMovie: newMapCache(NewBilibiliNoSharedMovieCacheInitFunc(movie), time.Minute*55),
|
|
SharedMpd: refreshcache1.NewRefreshCache(NewBilibiliSharedMpdCacheInitFunc(movie), time.Minute*55),
|
|
Subtitle: refreshcache1.NewRefreshCache(NewBilibiliSubtitleCacheInitFunc(movie), -1),
|
|
Live: refreshcache0.NewRefreshCache(NewBilibiliLiveCacheInitFunc(movie), time.Minute*55),
|
|
}
|
|
}
|
|
|
|
type BilibiliUserCache = refreshcache.RefreshCache[*BilibiliUserCacheData, struct{}]
|
|
|
|
type BilibiliUserCacheData struct {
|
|
Backend string
|
|
Cookies []*http.Cookie
|
|
}
|
|
|
|
func NewBilibiliUserCache(userID string) *BilibiliUserCache {
|
|
f := BilibiliAuthorizationCacheWithUserIDInitFunc(userID)
|
|
return refreshcache.NewRefreshCache(func(ctx context.Context, args ...struct{}) (*BilibiliUserCacheData, error) {
|
|
return f(ctx)
|
|
}, -1)
|
|
}
|
|
|
|
func BilibiliAuthorizationCacheWithUserIDInitFunc(userID string) func(ctx context.Context, args ...struct{}) (*BilibiliUserCacheData, error) {
|
|
return func(ctx context.Context, args ...struct{}) (*BilibiliUserCacheData, error) {
|
|
v, err := db.GetBilibiliVendor(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &BilibiliUserCacheData{
|
|
Cookies: utils.MapToHTTPCookie(v.Cookies),
|
|
Backend: v.Backend,
|
|
}, nil
|
|
}
|
|
}
|