add support for bilivideo playlist

This commit is contained in:
aynakeya
2024-06-21 23:16:19 +08:00
parent 56990efc7d
commit dfbe4e5eef
4 changed files with 345 additions and 10 deletions

View File

@@ -0,0 +1,124 @@
package bilivideo
import (
"crypto/md5"
"encoding/hex"
"github.com/AynaLivePlayer/miaosic"
"github.com/tidwall/gjson"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
mixinKeyEncTab = []int{
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52,
}
cache sync.Map
lastUpdateTime time.Time
)
func signAndGenerateURL(urlStr string) (string, error) {
urlObj, err := url.Parse(urlStr)
if err != nil {
return "", err
}
imgKey, subKey := getWbiKeysCached()
query := urlObj.Query()
params := map[string]string{}
for k, v := range query {
params[k] = v[0]
}
newParams := encWbi(params, imgKey, subKey)
for k, v := range newParams {
query.Set(k, v)
}
urlObj.RawQuery = query.Encode()
newUrlStr := urlObj.String()
return newUrlStr, nil
}
func encWbi(params map[string]string, imgKey, subKey string) map[string]string {
mixinKey := getMixinKey(imgKey + subKey)
currTime := strconv.FormatInt(time.Now().Unix(), 10)
params["wts"] = currTime
// Sort keys
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// Remove unwanted characters
for k, v := range params {
v = sanitizeString(v)
params[k] = v
}
// Build URL parameters
query := url.Values{}
for _, k := range keys {
query.Set(k, params[k])
}
queryStr := query.Encode()
// Calculate w_rid
hash := md5.Sum([]byte(queryStr + mixinKey))
params["w_rid"] = hex.EncodeToString(hash[:])
return params
}
func getMixinKey(orig string) string {
var str strings.Builder
for _, v := range mixinKeyEncTab {
if v < len(orig) {
str.WriteByte(orig[v])
}
}
return str.String()[:32]
}
func sanitizeString(s string) string {
unwantedChars := []string{"!", "'", "(", ")", "*"}
for _, char := range unwantedChars {
s = strings.ReplaceAll(s, char, "")
}
return s
}
func updateCache() {
if time.Since(lastUpdateTime).Minutes() < 10 {
return
}
imgKey, subKey := getWbiKeys()
cache.Store("imgKey", imgKey)
cache.Store("subKey", subKey)
lastUpdateTime = time.Now()
}
func getWbiKeysCached() (string, string) {
updateCache()
imgKeyI, _ := cache.Load("imgKey")
subKeyI, _ := cache.Load("subKey")
return imgKeyI.(string), subKeyI.(string)
}
func getWbiKeys() (string, string) {
resp, err := miaosic.Requester.Get("https://api.bilibili.com/x/web-interface/nav", biliHeaders)
if err != nil {
return "", ""
}
jsonResult := gjson.ParseBytes(resp.Body())
imgURL := jsonResult.Get("data.wbi_img.img_url").String()
subURL := jsonResult.Get("data.wbi_img.sub_url").String()
imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0]
subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0]
return imgKey, subKey
}

View File

@@ -15,6 +15,13 @@ import (
var _ = (miaosic.MediaProvider)(&BilibiliVideo{})
var biliHeaders = map[string]string{
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
"Cookie": "buvid3=40BA0253-7F5C-06C1-12CE-871EC008DB2096426infoc;",
}
type BilibiliVideo struct {
providers.DeepcolorProvider
BVRegex *regexp.Regexp
@@ -25,17 +32,11 @@ type BilibiliVideo struct {
}
func NewBilibiliViedo() *BilibiliVideo {
headers := map[string]string{
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
"Cookie": "buvid3=40BA0253-7F5C-06C1-12CE-871EC008DB2096426infoc;",
}
pvdr := &BilibiliVideo{
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
PageRegex: regexp.MustCompile("p=[0-9]+"),
header: headers,
header: biliHeaders,
}
pvdr.InfoApi = deepcolor.CreateApiResultFunc(
func(meta miaosic.MetaData) (*dphttp.Request, error) {

View File

@@ -1,11 +1,170 @@
package bilivideo
import "github.com/AynaLivePlayer/miaosic"
import (
"errors"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/aynakeya/deepcolor/dphttp"
"github.com/tidwall/gjson"
"regexp"
"strconv"
"strings"
)
const (
playlistCollection = "coll"
playlistFav = "fav"
)
func fetchParsedResult[P dphttp.ParserResultType](requester dphttp.IRequester, request *dphttp.Request, parserFunc dphttp.ParserFunc[P]) (P, error) {
httpResp, err := requester.HTTP(request)
if err != nil {
return *new(P), err
}
return parserFunc(httpResp)
}
var playlistCollectionRegex = regexp.MustCompile(`space.bilibili.com/(\d+)/channel/collectiondetail\?sid=(\d+)`)
var playlistFavRegex = regexp.MustCompile(`space.bilibili.com/(\d+)/favlist\?fid=(\d+)`)
func makePlaylistId(ptype string, id string) string {
return ptype + "_" + id
}
func parsePlaylistId(pid string) (string, string) {
parts := strings.SplitN(pid, "_", 2)
if parts[0] != playlistCollection && parts[0] != playlistFav {
return "", ""
}
if _, err := strconv.ParseInt(parts[1], 10, 64); err != nil {
return "", ""
}
return parts[0], parts[1]
}
func (n *BilibiliVideo) MatchPlaylist(uri string) (miaosic.MetaData, bool) {
if playlistCollectionRegex.MatchString(uri) {
matches := playlistCollectionRegex.FindStringSubmatch(uri)
return miaosic.MetaData{
Provider: n.GetName(),
Identifier: makePlaylistId(playlistCollection, matches[2]),
}, true
}
if playlistFavRegex.MatchString(uri) {
matches := playlistFavRegex.FindStringSubmatch(uri)
return miaosic.MetaData{
Provider: n.GetName(),
Identifier: makePlaylistId(playlistFav, matches[2]),
}, true
}
return miaosic.MetaData{}, false
}
func (n *BilibiliVideo) GetPlaylist(meta miaosic.MetaData) (*miaosic.Playlist, error) {
return nil, miaosic.ErrNotImplemented
var collApi = "https://api.bilibili.com/x/polymer/web-space/seasons_archives_list?mid=0&season_id=%s&sort_reverse=false&page_num=%d&page_size=30"
func (n *BilibiliVideo) getCollectionPlaylist(id string) (*miaosic.Playlist, error) {
page := 1
playlist := &miaosic.Playlist{
Meta: miaosic.MetaData{n.GetName(), makePlaylistId(playlistCollection, id)},
Medias: make([]miaosic.MediaInfo, 0),
Title: "Bilibili Collection " + id,
}
for {
uri := fmt.Sprintf(collApi, id, page)
resp, err := miaosic.Requester.Get(uri, biliHeaders)
if err != nil {
return nil, err
}
if resp.StatusCode() != 200 {
return nil, miaosic.ErrorExternalApi
}
result := gjson.ParseBytes(resp.Body())
if result.Get("code").Int() != 0 {
return nil, errors.New("bilivideo: " + result.Get("message").String())
}
archives := result.Get("data.archives")
if len(archives.Array()) == 0 {
playlist.Title = result.Get("data.meta.name").String()
break
}
archives.ForEach(func(key, value gjson.Result) bool {
playlist.Medias = append(playlist.Medias, miaosic.MediaInfo{
Title: value.Get("title").String(),
Cover: miaosic.Picture{Url: value.Get("pic").String()},
Artist: id,
Meta: miaosic.MetaData{
Provider: n.GetName(),
Identifier: value.Get("bvid").String(),
},
})
return true
})
page++
}
if len(playlist.Medias) == 0 {
return nil, errors.New("bilivideo: no media found")
}
return playlist, nil
}
var favApi = "https://api.bilibili.com/x/v3/fav/resource/list?media_id=%s&pn=%d&ps=20&keyword=&order=mtime&type=0&tid=0&platform=web"
func (n *BilibiliVideo) getFavPlaylist(id string) (*miaosic.Playlist, error) {
page := 1
playlist := &miaosic.Playlist{
Meta: miaosic.MetaData{n.GetName(), makePlaylistId(playlistFav, id)},
Medias: make([]miaosic.MediaInfo, 0),
Title: "Bilibili Fav " + id,
}
for {
uri := fmt.Sprintf(favApi, id, page)
resp, err := miaosic.Requester.Get(uri, biliHeaders)
if err != nil {
return nil, err
}
if resp.StatusCode() != 200 {
return nil, miaosic.ErrorExternalApi
}
result := gjson.ParseBytes(resp.Body())
if result.Get("code").Int() != 0 {
return nil, errors.New("bilivideo: " + result.Get("message").String())
}
medias := result.Get("data.medias")
if len(medias.Array()) == 0 {
playlist.Title = result.Get("data.info.title").String()
break
}
medias.ForEach(func(key, value gjson.Result) bool {
title := value.Get("title").String()
if title == "已失效视频" || title == "" {
return true
}
playlist.Medias = append(playlist.Medias, miaosic.MediaInfo{
Title: value.Get("title").String(),
Cover: miaosic.Picture{Url: value.Get("cover").String()},
Artist: value.Get("upper.name").String(),
Meta: miaosic.MetaData{
Provider: n.GetName(),
Identifier: value.Get("bvid").String() + "?p=" + value.Get("page").String(),
},
})
return true
})
if !result.Get("data.has_more").Bool() {
playlist.Title = result.Get("data.info.title").String()
break
}
}
return playlist, nil
}
func (n *BilibiliVideo) GetPlaylist(meta miaosic.MetaData) (*miaosic.Playlist, error) {
ptype, id := parsePlaylistId(meta.Identifier)
if ptype == "" {
return nil, errors.New("bilivideo: invalid playlist identifier")
}
if ptype == playlistCollection {
return n.getCollectionPlaylist(id)
}
return n.getFavPlaylist(id)
}

View File

@@ -0,0 +1,51 @@
package bilivideo
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestBilibiliVideo_MatchPlaylist_Ok(t *testing.T) {
meta, ok := api.MatchPlaylist("https://space.bilibili.com/346563107/favlist?fid=1179446107&ftype=create")
require.True(t, ok)
require.Equal(t, api.GetName(), meta.Provider)
require.Equal(t, playlistFav+"_1179446107", meta.Identifier)
meta, ok = api.MatchPlaylist("https://space.bilibili.com/346563107/channel/collectiondetail?sid=1889103&ctype=0")
require.True(t, ok)
require.Equal(t, api.GetName(), meta.Provider)
require.Equal(t, playlistCollection+"_1889103", meta.Identifier)
}
func TestBilibiliVideo_MatchPlaylist_Fail(t *testing.T) {
meta, ok := api.MatchPlaylist("https://space.bilibili.com/346563107")
require.False(t, ok)
require.Empty(t, meta)
meta, ok = api.MatchPlaylist("https://space.bilibili.com/346563107/favlist")
require.False(t, ok)
require.Empty(t, meta)
meta, ok = api.MatchPlaylist("https://space.bilibili.com/346563107/channel/collectiondetail")
require.False(t, ok)
require.Empty(t, meta)
}
func TestBilibiliVideo_GetPlaylist_Collection(t *testing.T) {
uri := "https://space.bilibili.com/346563107/channel/collectiondetail?sid=1889103&ctype=0"
meta, ok := api.MatchPlaylist(uri)
require.True(t, ok)
playlist, err := api.GetPlaylist(meta)
require.NoError(t, err)
require.Equal(t, meta, playlist.Meta)
require.Equal(t, "合集·巴以冲突-世界在关注什么?", playlist.Title)
require.GreaterOrEqual(t, len(playlist.Medias), 71)
}
func TestBilibiliVideo_GetPlaylist_Fav(t *testing.T) {
uri := "https://space.bilibili.com/10003632/favlist?fid=729246932&ftype=create"
meta, ok := api.MatchPlaylist(uri)
require.True(t, ok)
playlist, err := api.GetPlaylist(meta)
require.NoError(t, err)
require.Equal(t, meta, playlist.Meta)
require.Equal(t, "AMV", playlist.Title)
require.GreaterOrEqual(t, len(playlist.Medias), 12)
}