可信源码风险提示:保存或下载前请阅读源码和权限声明。
let SESSDATA = '';
const view = ui.signal('menu');
const title = ui.signal('');
const lines = ui.signal([]);
const qrUrl = ui.signal('');
const qrStatus = ui.signal('');
let pollTimer = null;
let lastReq = null;
let curBvid = '';
let curAid = 0;
let curVideo = null;
let curList = [];
let prevView = 'menu';
script.data.get('sessdata', '').then(v => {
if (v) SESSDATA = v;
ui.refresh();
});
function fmt(n) {
n = Number(n) || 0;
if (n >= 100000000) return (n / 100000000).toFixed(1) + '亿';
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
return String(n);
}
function fmtDur(s) {
s = Number(s) || 0;
const m = Math.floor(s / 60);
const r = s % 60;
return m + ':' + (r < 10 ? '0' : '') + r;
}
function hdr(referer) {
const h = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Referer': referer || 'https://www.bilibili.com'
};
if (SESSDATA) h['Cookie'] = 'SESSDATA=' + SESSDATA;
return h;
}
function httpGet(url, referer) {
return new Promise(resolve => {
system.http.request({
url: url,
header: hdr(referer),
responseType: 'text',
success: r => {
try { resolve(JSON.parse(r.data)); } catch (e) { resolve(null); }
},
fail: () => resolve(null)
});
});
}
function qs(obj) {
return Object.keys(obj).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(obj[k])).join('&');
}
function httpPost(url, params, referer) {
return new Promise(resolve => {
const h = hdr(referer);
h['Content-Type'] = 'application/x-www-form-urlencoded';
system.http.request({
url: url,
method: 'POST',
data: qs(params),
header: h,
responseType: 'text',
success: r => {
try { resolve(JSON.parse(r.data)); } catch (e) { resolve(null); }
},
fail: () => resolve(null)
});
});
}
function getCsrf() {
return new Promise(resolve => {
if (!SESSDATA) { resolve(''); return; }
script.data.get('bili_jct', '').then(saved => {
if (saved) { resolve(saved); return; }
httpGet('https://api.bilibili.com/x/web-interface/nav').then(d => {
if (d && d.code === 0 && d.data && d.data.bili_jct) {
script.data.set('bili_jct', d.data.bili_jct);
script.data.set('mid', String(d.data.mid));
resolve(d.data.bili_jct);
} else {
resolve('');
}
});
});
});
}
function showLines(v, t, items) {
view.set(v);
title.set(t);
lines.set(items);
ui.refresh();
}
function dataRow(k, v) {
return ui.row([
ui.text(k, { size: 12, color: '#999999', lines: 1 }),
ui.text(v, { size: 13, color: '#ffffff', align: 'right', lines: 1, flex: 1 })
], { gap: 10, padding: 2 });
}
function listItem(label, onPress) {
return ui.button(label, onPress, {
id: 'li' + Math.floor(Math.random() * 100000),
background: '#232327', color: '#e8e8e8', size: 12, lines: 2, height: 56, flex: 1
});
}
function openDetail(bvid) {
curBvid = bvid;
curVideo = null;
prevView = view.get();
lastReq = { title: '视频 · ' + bvid, load: () => openDetail(bvid) };
showLines('detail', '视频 · ' + bvid, [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/web-interface/view?bvid=' + encodeURIComponent(bvid)).then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误')]);
ui.refresh();
return;
}
curVideo = d.data;
curAid = d.data.aid;
ui.refresh();
});
}
function hotList() {
lastReq = { title: '热门', load: hotList };
curList = [];
showLines('list', '热门', [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/web-interface/ranking/v2?rid=0&type=all').then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误')]);
ui.refresh();
return;
}
curList = d.data.list.slice(0, 10).map((v, i) => ({
l1: '#' + (i + 1) + ' ' + v.title.slice(0, 28),
l2: (v.owner ? v.owner.name : '未知') + ' · ' + fmt(v.stat.view) + '播放 · ' + (v.tname || ''),
bvid: v.bvid
}));
lines.set(['查看详情']);
ui.refresh();
});
}
function runSearch(q) {
lastReq = { title: '搜索 · ' + q, load: () => runSearch(q) };
curList = [];
showLines('list', '搜索 · ' + q, [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/web-interface/search/type?search_type=video&keyword=' + encodeURIComponent(q) + '&page=1', 'https://search.bilibili.com').then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误')]);
ui.refresh();
return;
}
const list = d.data.result || [];
if (!list.length) {
lines.set(['无搜索结果']);
ui.refresh();
return;
}
curList = list.slice(0, 10).map(v => ({
l1: String(v.title).replace(/<[^>]+>/g, '').slice(0, 28),
l2: (v.author || '未知') + ' · ' + fmt(v.play) + '播放 · ' + (v.duration || ''),
bvid: v.bvid
}));
lines.set(['查看详情']);
ui.refresh();
});
}
async function searchVideo() {
const res = await dialog.text({
title: '搜索视频',
message: '输入关键词',
placeholder: '如 原神 / 鬼畜',
maxLength: 30
});
if (res.action !== 'confirm') return;
const q = res.value.trim();
if (!q) return;
runSearch(q);
}
async function queryVideo() {
const res = await dialog.text({
title: '视频信息',
message: '输入 BV 号',
placeholder: '如 BV1GJ411x7h7',
maxLength: 20
});
if (res.action !== 'confirm') return;
const q = res.value.trim();
if (!q) return;
if (q.toUpperCase().indexOf('BV') !== 0) {
await dialog.alert({ title: '格式错误', message: 'BV号以 BV 开头' });
return;
}
openDetail(q.toUpperCase());
}
function openUp(mid) {
prevView = view.get();
lastReq = { title: 'UP主 · ' + mid, load: () => openUp(mid) };
showLines('up', 'UP主 · ' + mid, [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/web-interface/card?mid=' + mid).then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误')]);
ui.refresh();
return;
}
const c = d.data.card;
lines.set([
['昵称', c.name],
['性别', c.sex === '男' ? '♂' : c.sex === '女' ? '♀' : c.sex],
['粉丝', fmt(d.data.follower) + ' / 关注 ' + fmt(c.attention)],
['获赞', fmt(d.data.like_num)],
['生日', (c.birthday || '未知') + ' · ' + (c.place || '未知')],
['签名', String(c.sign || '无').slice(0, 50)],
['UID', c.mid]
]);
ui.refresh();
});
}
async function queryUser() {
const res = await dialog.text({
title: '用户信息',
message: '输入 UID',
placeholder: '如 2',
maxLength: 20
});
if (res.action !== 'confirm') return;
const q = res.value.trim();
if (!q || !/^\d+$/.test(q)) {
await dialog.alert({ title: '格式错误', message: 'UID 为纯数字' });
return;
}
openUp(q);
}
function myLevel() {
if (!SESSDATA) {
dialog.alert({ title: '未登录', message: '请先登录' });
return;
}
lastReq = { title: '我的等级', load: myLevel };
showLines('result', '我的等级', [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/web-interface/nav').then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误'), String(d ? d.message || '' : '').slice(0, 60)]);
ui.refresh();
return;
}
const lv = d.data.level_info || {};
lines.set([
['昵称', d.data.uname],
['等级', 'Lv.' + lv.current_level],
['经验', fmt(lv.current_exp) + ' / ' + fmt(lv.next_exp)],
['硬币', d.data.money],
['大会员', d.data.vipStatus === 1 ? '是' : '否'],
['UID', d.data.mid]
]);
ui.refresh();
});
}
async function act(name, params) {
const csrf = await getCsrf();
if (!csrf) {
dialog.alert({ title: name + '失败', message: '请先登录' });
return;
}
params.csrf = csrf;
const d = await httpPost('https://api.bilibili.com/x/web-interface/archive/like', params);
if (d && d.code === 0) {
dialog.alert({ title: name + '成功', message: '已' + name });
} else {
dialog.alert({ title: name + '失败', message: 'B站返回: ' + (d ? d.code + ' ' + (d.message || '') : '网络错误') });
}
}
function doLike() { act('点赞', { bvid: curBvid, like: 1 }); }
function doCoin() { act('投币', { bvid: curBvid, multiply: 1 }); }
async function doFav() {
const csrf = await getCsrf();
if (!csrf) {
dialog.alert({ title: '收藏失败', message: '请先登录' });
return;
}
const mid = await script.data.get('mid', '');
if (!mid) {
dialog.alert({ title: '收藏失败', message: '请先打开「我的等级」' });
return;
}
const fl = await httpGet('https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid=' + mid);
let fid = 0;
if (fl && fl.code === 0 && fl.data && fl.data.list && fl.data.list.length) {
fid = fl.data.list[0].id;
}
if (!fid) {
dialog.alert({ title: '收藏失败', message: '无可用收藏夹' });
return;
}
const d = await httpPost('https://api.bilibili.com/x/v3/fav/resource/deal', {
rid: curAid, type: 2, add_media_ids: fid, csrf: csrf
});
if (d && d.code === 0) {
dialog.alert({ title: '收藏成功', message: '已加入默认收藏夹' });
} else {
dialog.alert({ title: '收藏失败', message: 'B站返回: ' + (d ? d.code + ' ' + (d.message || '') : '网络错误') });
}
}
function videoSummary() {
if (!curVideo) return;
prevView = view.get();
lastReq = { title: '视频总结', load: videoSummary };
showLines('summary', '视频总结', [['状态', '生成中...']]);
httpGet('https://api.bilibili.com/x/web-interface/view/conclusion/get?bvid=' + curBvid).then(d => {
let txt = '';
if (d && d.code === 0 && d.data) {
const mr = d.data.model_result || {};
txt = mr.summary || mr.simple_summary || '';
}
if (!txt) {
lines.set(['暂无总结', '包含敏感信息']);
} else {
lines.set([txt]);
}
ui.refresh();
});
}
function openComments() {
if (!curVideo) return;
prevView = view.get();
lastReq = { title: '评论区', load: openComments };
showLines('comments', '评论', [['状态', '加载中...']]);
httpGet('https://api.bilibili.com/x/v2/reply?type=1&oid=' + curAid + '&sort=2&pn=1').then(d => {
if (!d || d.code !== 0) {
lines.set(['B站返回: ' + (d ? d.code : '网络错误')]);
ui.refresh();
return;
}
const rs = d.data && d.data.replies ? d.data.replies : [];
if (!rs.length) {
lines.set(['暂无评论']);
ui.refresh();
return;
}
lines.set(rs.slice(0, 8).map(r => [
r.member.uname,
String(r.content.message).slice(0, 48) + (r.like > 0 ? ' · 赞' + fmt(r.like) : '')
]));
ui.refresh();
});
}
async function postComment() {
if (!curVideo) return;
const csrf = await getCsrf();
if (!csrf) {
dialog.alert({ title: '发评论失败', message: '请先登录' });
return;
}
const res = await dialog.text({
title: '发评论',
message: '输入评论内容',
placeholder: '友善发言',
maxLength: 100
});
if (res.action !== 'confirm') return;
const msg = res.value.trim();
if (!msg) return;
const d = await httpPost('https://api.bilibili.com/x/v2/reply/add', {
type: 1, oid: curAid, message: msg, csrf: csrf
});
if (d && d.code === 0) {
dialog.alert({ title: '发布成功', message: '评论已发出' });
} else {
dialog.alert({ title: '发布失败', message: 'B站返回: ' + (d ? d.code + ' ' + (d.message || '') : '网络错误') });
}
}
function generateQr() {
if (pollTimer) clearInterval(pollTimer);
qrStatus.set('生成中...');
system.http.request({
url: 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate',
header: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Referer': 'https://www.bilibili.com'
},
responseType: 'text',
success: response => {
try {
const d = JSON.parse(response.data);
if (d.code !== 0) {
qrStatus.set('生成失败: ' + d.code);
view.set('qrcode');
ui.refresh();
return;
}
qrUrl.set(d.data.url);
qrStatus.set('请用 Bilibili 扫码');
view.set('qrcode');
ui.refresh();
startPoll(d.data.qrcode_key);
} catch (e) {
qrStatus.set('响应异常');
view.set('qrcode');
ui.refresh();
}
},
fail: (data, code) => {
qrStatus.set('生成失败: ' + code);
view.set('qrcode');
ui.refresh();
}
});
}
function startPoll(key) {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
system.http.request({
url: 'https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=' + key,
header: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Referer': 'https://www.bilibili.com'
},
responseType: 'text',
success: response => {
try {
const d = JSON.parse(response.data);
const st = d.data ? d.data.code : -1;
if (st === 0) {
clearInterval(pollTimer);
pollTimer = null;
qrStatus.set('扫码成功!');
ui.refresh();
dialog.alert({
title: '扫码成功',
message: 'SESSDATA获取方式: 浏览器访问 https://login.bilibili.bi/ 找到 SESSDATA= 后面、; bili_jct= 前面的部分'
});
} else if (st === 86090) {
qrStatus.set('已扫码,请在手机确认');
ui.refresh();
} else if (st === 86101) {
qrStatus.set('等待扫码...');
ui.refresh();
} else if (st === 86038) {
clearInterval(pollTimer);
pollTimer = null;
qrStatus.set('二维码已失效');
ui.refresh();
dialog.alert({ title: '二维码失效', message: '「重新生成」' });
}
} catch (e) {}
},
fail: () => {}
});
}, 3000);
}
function cancelQr() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = null;
view.set('menu');
ui.refresh();
}
async function setCookie() {
const res = await dialog.text({
title: '输入 cookie',
message: '输入获取 SESSDATA 的值 ',
placeholder: 'SESSDATA 值',
maxLength: 256
});
if (res.action !== 'confirm') return;
const v = res.value.trim();
if (!v) return;
SESSDATA = v;
script.data.set('sessdata', v);
dialog.alert({ title: '已保存', message: 'cookie 已保存,下次启动时将自动使用' });
}
function back() {
view.set(prevView === view.get() ? 'menu' : prevView);
ui.refresh();
}
function refreshNow() {
if (lastReq) lastReq.load();
}
function navHeader(titleText) {
return [
ui.row([
ui.button('← 返回', back, { id: 'b', flex: 1, tone: 'neutral', height: 40 }),
ui.button('↻ 刷新', refreshNow, { id: 'r', flex: 1, tone: 'neutral', height: 40 })
], { gap: 8 }),
ui.spacer(6),
ui.heading(titleText, { align: 'center' }),
ui.spacer(2),
ui.divider({ color: '#2a2a2e' }),
ui.spacer(6)
];
}
ui.setTitle(' Bilibili ');
ui.render(() => {
if (view.get() === 'qrcode') {
const items = [
ui.button('← 取消', cancelQr, { id: 'q', tone: 'neutral', height: 40 }),
ui.spacer(6),
ui.heading('扫码登录', { align: 'center' }),
ui.spacer(4)
];
if (qrUrl.get()) {
items.push(ui.qrcode(qrUrl.get(), { id: 'qr', size: 200, width: 200 }));
} else {
items.push(ui.text('二维码生成中...', {
size: 15, color: '#888888', align: 'center', lines: 1, height: 210
}));
}
items.push(ui.spacer(4));
items.push(ui.text(qrStatus.get(), {
size: 15, bold: true, color: qrStatus.get() === '扫码成功!' ? '#52c41a' : '#ffd75e',
align: 'center', lines: 1
}));
items.push(ui.spacer(6));
items.push(ui.button('重新生成', generateQr, { id: 're', tone: 'warning', height: 44 }));
return items;
}
if (view.get() === 'list') {
const items = navHeader(title.get());
if (curList.length) {
curList.forEach(it => {
items.push(listItem(it.l1 + '\n' + it.l2, () => openDetail(it.bvid)));
items.push(ui.spacer(4));
});
return items;
}
const data = lines.get();
data.forEach((l, i) => {
items.push(ui.text(typeof l === 'string' ? l : l[0] + ' ' + l[1], {
size: 13, color: i === 0 ? '#ffd75e' : '#bbbbbb', align: 'center', lines: 1, bold: i === 0
}));
items.push(ui.spacer(4));
});
return items;
}
if (view.get() === 'detail') {
const items = navHeader(title.get());
if (!curVideo) {
items.push(ui.text('加载中...', { size: 13, color: '#888888', align: 'center', lines: 1 }));
return items;
}
const v = curVideo;
items.push(ui.text(v.title, { size: 14, bold: true, color: '#ffffff', align: 'center', lines: 2 }));
items.push(ui.spacer(4));
items.push(dataRow('UP主', v.owner ? v.owner.name : '未知'));
items.push(dataRow('播放', fmt(v.stat.view) + ' / 弹幕 ' + fmt(v.stat.danmaku)));
items.push(dataRow('点赞', fmt(v.stat.like) + ' / 投币 ' + fmt(v.stat.coin) + ' / 收藏 ' + fmt(v.stat.favorite)));
items.push(dataRow('时长', fmtDur(v.duration)));
items.push(dataRow('简介', String(v.desc || '无').slice(0, 60)));
items.push(ui.spacer(4));
items.push(ui.row([
ui.button('点赞', doLike, { id: 'lk', flex: 1, tone: 'danger', height: 46 }),
ui.button('投币', doCoin, { id: 'cn', flex: 1, tone: 'warning', height: 46 }),
ui.button('收藏', doFav, { id: 'fv', flex: 1, tone: 'success', height: 46 })
], { gap: 8 }));
items.push(ui.spacer(6));
items.push(ui.row([
ui.button('UP主', () => openUp(v.owner.mid), { id: 'up', flex: 1, tone: 'primary', height: 46 }),
ui.button('视频总结', videoSummary, { id: 'sm', flex: 1, tone: 'primary', height: 46 }),
ui.button('评论', openComments, { id: 'cm', flex: 1, tone: 'primary', height: 46 })
], { gap: 8 }));
items.push(ui.spacer(6));
items.push(ui.button('发评论', postComment, { id: 'pc', tone: 'neutral', height: 44 }));
return items;
}
if (view.get() === 'up' || view.get() === 'comments' || view.get() === 'summary' || view.get() === 'result') {
const items = navHeader(title.get());
lines.get().forEach((l, i) => {
if (typeof l === 'string') {
items.push(ui.text(l, {
size: 13, color: i === 0 ? '#ffd75e' : '#bbbbbb', align: 'center', lines: 3, bold: i === 0
}));
items.push(ui.spacer(4));
} else {
items.push(dataRow(l[0], l[1]));
items.push(ui.spacer(3));
}
});
return items;
}
return [
ui.heading('JS Bilibili', { align: 'center' }),
ui.spacer(4),
ui.row([
ui.text(SESSDATA ? '● 已登录 ' : '○ 未登录', {
size: 12, color: SESSDATA ? '#52c41a' : '#888888', align: 'center', flex: 1
})
], { padding: 10, background: SESSDATA ? '#142b1c' : '#232327', radius: 12 }),
ui.spacer(10),
ui.column([
ui.row([
ui.button('热门', hotList, { id: 'h', flex: 1, tone: 'primary', height: 52 }),
ui.button('搜索', searchVideo, { id: 's', flex: 1, tone: 'success', height: 52 })
], { gap: 8 }),
ui.spacer(4),
ui.row([
ui.button('视频', queryVideo, { id: 'v', flex: 1, tone: 'warning', height: 52 }),
ui.button('搜索UID', queryUser, { id: 'u', flex: 1, tone: 'neutral', height: 52 })
], { gap: 8 }),
ui.spacer(4),
ui.row([
ui.button('我的', myLevel, { id: 'l', flex: 1, tone: 'danger', height: 52 }),
ui.button('扫码登录', generateQr, { id: 'q2', flex: 1, tone: 'warning', height: 52 })
], { gap: 8 }),
ui.spacer(4),
ui.row([
ui.button('设置cookie', setCookie, { id: 'c', flex: 1, tone: 'neutral', height: 44 })
], { gap: 8 })
], { padding: 12, background: '#232327', radius: 12, gap: 8 }),
ui.spacer(12),
ui.column([
ui.text('', { heading: true, align: 'center' }),
ui.spacer(2),
ui.text('关注塔菲喵~关注塔菲谢谢喵~塔不灭塔不灭雏草姬不灭喵!关注塔菲关注塔菲谢谢喵~关注塔菲关注塔菲关注塔菲关注塔菲谢谢喵!\n', {
size: 12, color: '#888888', align: 'center', lines: 2
})
], { padding: 12, background: '#1e1e22', radius: 12 })
];
});