bilibili_query (2)

差不多和HyperBilibli一样

适配设备:Pro

作者:rikka

下载量:97 · 点赞:4

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-256b1e1363a9a7bdd4ce1d2bc5bba800aef195db778927df08f4da61d6515b799c0
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 })
  ];
});