mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2026-05-10 09:49:37 +08:00
add support for bilivideo playlist
This commit is contained in:
124
providers/bilivideo/bilibili_wbi.go
Normal file
124
providers/bilivideo/bilibili_wbi.go
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
51
providers/bilivideo/playlist_test.go
Normal file
51
providers/bilivideo/playlist_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user