Skip to content

Commit f3595f5

Browse files
committed
feat(provider): add a new provider named bilivideo for search and track bilibili videos
Signed-off-by: lony2003 <zhangke200377@outlook.com>
1 parent d968f17 commit f3595f5

File tree

5 files changed

+234
-1
lines changed

5 files changed

+234
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ node app.js -o bilibili ytdlp
176176
| YouTube(通过 `youtube-dl`) | `youtubedl` | | 需要自行安装 `youtube-dl`|
177177
| YouTube(通过 `yt-dlp`) | `ytdlp` || 需要自行安装 `yt-dlp``youtube-dl` 仍在活跃维护的 fork)。 |
178178
| B 站音乐 | `bilibili` | | |
179+
| B 站音乐 | `bilivideo` | | 在大陆地区外的IP地址可能查询不到某些版权视频(如索尼音乐上传的MV等) |
179180
| 第三方网易云 API | `pyncmd` | | |
180181
181182
- 支持 `pyncmd` 的 API 服务由 GD studio <https://music.gdstudio.xyz> 提供。

src/consts.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const PROVIDERS = {
99
youtubedl: require('./provider/youtube-dl'),
1010
ytdlp: require('./provider/yt-dlp'),
1111
bilibili: require('./provider/bilibili'),
12+
bilivideo: require('./provider/bilivideo'),
1213
pyncmd: require('./provider/pyncmd'),
1314
};
1415

src/provider/bilivideo.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
const {
2+
cacheStorage,
3+
CacheStorageGroup,
4+
getManagedCacheStorage,
5+
} = require('../cache');
6+
const insure = require('./insure');
7+
const select = require('./select');
8+
const request = require('../request');
9+
const crypto = require('../crypto');
10+
const { logScope } = require('../logger');
11+
12+
const logger = logScope('provider/bilivideo');
13+
const cs = getManagedCacheStorage('provider/bilivideo');
14+
15+
//Wbi 和 API 部分参考: https://github.com/SocialSisterYi/bilibili-API-collect
16+
17+
const mixinKeyEncTab = [
18+
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
19+
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
20+
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
21+
36, 20, 34, 44, 52,
22+
];
23+
24+
// 对 imgKey 和 subKey 进行字符顺序打乱编码
25+
const getMixinKey = (orig) =>
26+
mixinKeyEncTab
27+
.map((n) => orig[n])
28+
.join('')
29+
.slice(0, 32);
30+
31+
// 为请求参数进行 wbi 签名
32+
function encWbi(params, img_key, sub_key) {
33+
const mixin_key = getMixinKey(img_key + sub_key),
34+
curr_time = Math.round(Date.now() / 1000),
35+
chr_filter = /[!'()*]/g;
36+
37+
Object.assign(params, { wts: curr_time }); // 添加 wts 字段
38+
// 按照 key 重排参数
39+
const query = Object.keys(params)
40+
.sort()
41+
.map((key) => {
42+
// 过滤 value 中的 "!'()*" 字符
43+
const value = params[key].toString().replace(chr_filter, '');
44+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
45+
})
46+
.join('&');
47+
48+
const wbi_sign = crypto.md5.digest(query + mixin_key); // 计算 w_rid
49+
50+
return query + '&w_rid=' + wbi_sign;
51+
}
52+
53+
// 获取最新的 img_key 和 sub_key
54+
async function getWbiKeys() {
55+
const res = await request(
56+
'GET',
57+
'https://api.bilibili.com/x/web-interface/nav',
58+
{
59+
// SESSDATA 字段
60+
'User-Agent':
61+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
62+
Referer: 'https://www.bilibili.com/', //对于直接浏览器调用可能不适用
63+
}
64+
);
65+
const {
66+
data: {
67+
wbi_img: { img_url, sub_url },
68+
},
69+
} = await res.json();
70+
71+
return {
72+
img_key: img_url.slice(
73+
img_url.lastIndexOf('/') + 1,
74+
img_url.lastIndexOf('.')
75+
),
76+
sub_key: sub_url.slice(
77+
sub_url.lastIndexOf('/') + 1,
78+
sub_url.lastIndexOf('.')
79+
),
80+
};
81+
}
82+
83+
const signParam = async (param) => {
84+
const { img_key, sub_key } = await cs.cache(
85+
'wbikey',
86+
async () => await getWbiKeys()
87+
);
88+
89+
return encWbi(param, img_key, sub_key);
90+
};
91+
92+
const format = (song) => {
93+
return {
94+
id: song.bvid,
95+
name: song.title,
96+
// album: {id: song.album_id, name: song.album_title},
97+
artists: { id: song.typeid, name: song.typename },
98+
};
99+
};
100+
101+
const getBiliVideoHeader = async () => {
102+
const url = 'https://www.bilibili.com';
103+
104+
return cs.cache('bilicookie', () =>
105+
request('GET', url).then((response) =>
106+
response.headers['set-cookie']
107+
.map((cookie) => cookie.split(';')[0])
108+
.join('; ')
109+
)
110+
);
111+
};
112+
113+
const search = (info) => {
114+
return getBiliVideoHeader().then((cookies) => {
115+
return signParam({
116+
search_type: 'video',
117+
keyword: info.keyword,
118+
}).then((param) => {
119+
const url =
120+
'https://api.bilibili.com/x/web-interface/wbi/search/type?' +
121+
param;
122+
return request('GET', url, {
123+
cookie: cookies,
124+
referer: 'https://search.bilibili.com',
125+
})
126+
.then((response) => response.json())
127+
.then((jsonBody) => {
128+
const list = jsonBody.data.result.map(format);
129+
const matched = select(list, info);
130+
131+
return matched ? matched.id : Promise.reject();
132+
});
133+
});
134+
});
135+
};
136+
137+
const track = (id) => {
138+
return signParam({ bvid: id }).then((param) => {
139+
const url =
140+
'https://api.bilibili.com/x/web-interface/wbi/view?' + param;
141+
142+
return request('GET', url)
143+
.then((response) => response.json())
144+
.then((jsonBody) => {
145+
if (jsonBody.code === 0) {
146+
// bilibili music requires referer, connect do not support referer, so change to http
147+
148+
return signParam({
149+
bvid: id,
150+
cid: jsonBody.data.cid,
151+
fnval: 16,
152+
platform: 'pc',
153+
}).then((param) => {
154+
const url =
155+
'https://api.bilibili.com/x/player/wbi/playurl?' +
156+
param;
157+
158+
return request('GET', url)
159+
.then((response) => response.json())
160+
.then((jsonBody) => {
161+
if (jsonBody.code === 0) {
162+
if (jsonBody.data.dash.audio != null) {
163+
return jsonBody.data.dash.audio[0]
164+
.base_url;
165+
}
166+
return Promise.reject();
167+
} else {
168+
return Promise.reject();
169+
}
170+
})
171+
.catch(() => insure().bilibili.track(id));
172+
});
173+
} else {
174+
return Promise.reject();
175+
}
176+
})
177+
.catch(() => insure().bilibili.track(id));
178+
});
179+
};
180+
181+
const check = (info) => cs.cache(info, () => search(info)).then(track);
182+
183+
module.exports = { check, track };

src/provider/bilivideo.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const { DEFAULT_SOURCE } = require('../consts');
2+
const match = require('./match');
3+
4+
const songList = [
5+
520521849, // Remix; https://music.163.com/song/520521849
6+
185811, // 周杰倫; https://music.163.com/song/185811
7+
33955999, // ACG; https://music.163.com/song/33955999
8+
515540639, // TheFatRat; https://music.163.com/song/515540639
9+
33190502, // ACG; http://music.163.com/song/33190502
10+
];
11+
12+
describe('Test if the default sources can get any song', () => {
13+
songList.map(
14+
(song) =>
15+
test(
16+
`finding: ${song}`,
17+
async () => match(song, ['bilivideo']),
18+
15000
19+
) // can wait for only 15s
20+
);
21+
});
22+
23+
/* FOR DEVS: Uncomment these if you want to test all the sources */
24+
// const sources = Object.keys(PROVIDERS);
25+
//
26+
// /**
27+
// * Check if the specified song existed in the specified source.
28+
// * @param source {string}
29+
// * @param song {number}
30+
// * @return {Promise<void>}
31+
// */
32+
// const isSongExistedInSource = async (source, song) => {
33+
// const response = await match(song, [source]);
34+
// if (!response || !response.url)
35+
// throw new Error(`${song} is not in ${source}`);
36+
// };
37+
//
38+
//
39+
// sources.forEach((source) => {
40+
// test(`Test if ${source} can get any song`, async (done) => {
41+
// return Promise.any(
42+
// songList.map(async (song) => isSongExistedInSource(source, song))
43+
// );
44+
// }, 30000); // can wait for 30s
45+
// });

src/provider/match.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const logger = logScope('provider/match');
2121
const isHttpResponseOk = (code) => code >= 200 && code <= 299;
2222

2323
/** @type {Map<string, string>} */
24-
const headerReferer = new Map([['bilivideo.com', 'https://www.bilibili.com/']]);
24+
const headerReferer = new Map([
25+
['bilivideo.com', 'https://www.bilibili.com/'],
26+
['upos-hz-mirrorakam.akamaized.net', 'https://www.bilibili.com/'],
27+
]);
2528

2629
/**
2730
* @typedef {{ size: number, br: number | null, url: string | null, md5: string | null, source: string }} AudioData

0 commit comments

Comments
 (0)