mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
添加客户端
This commit is contained in:
92
src/client/data/biliLogin.ts
Normal file
92
src/client/data/biliLogin.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { error } from '@tauri-apps/plugin-log'
|
||||
import { QueryBiliAPI } from './utils';
|
||||
|
||||
export async function checkLoginStatusAsync(): Promise<boolean> {
|
||||
const url = 'https://api.bilibili.com/x/web-interface/nav/stat';
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
|
||||
return json.code === 0;
|
||||
}
|
||||
|
||||
export async function getUidAsync(): Promise<number> {
|
||||
const url = 'https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info';
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
|
||||
if (json.data && json.data.uid) {
|
||||
return json.data.uid;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
// 二维码地址及扫码密钥
|
||||
export async function getLoginUrlAsync(): Promise<any> {
|
||||
const url = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate';
|
||||
const response = await QueryBiliAPI(url, 'GET')
|
||||
if (!response.ok) {
|
||||
const result = await response.text();
|
||||
error('无法获取B站登陆二维码: ' + result);
|
||||
throw new Error('获取二维码地址失败');
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getLoginUrlDataAsync(): Promise<{
|
||||
url: string;
|
||||
qrcode_key: string;
|
||||
}> {
|
||||
const message = await getLoginUrlAsync();
|
||||
if (message.code !== 0) {
|
||||
throw new Error('获取二维码地址失败');
|
||||
}
|
||||
return message.data as {
|
||||
url: string;
|
||||
qrcode_key: string;
|
||||
};
|
||||
}
|
||||
type QRCodeLoginInfo =
|
||||
| { status: 'expired' }
|
||||
| { status: 'unknown' }
|
||||
| { status: 'scanned' }
|
||||
| { status: 'waiting' }
|
||||
| { status: 'confirmed'; cookie: string; refresh_token: string };
|
||||
export async function getLoginInfoAsync(qrcodeKey: string): Promise<QRCodeLoginInfo> {
|
||||
const url = `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${qrcodeKey}&source=main-fe-header`;
|
||||
const response = await QueryBiliAPI(url);
|
||||
const message = await response.json();
|
||||
|
||||
if (!message.data) {
|
||||
throw new Error('获取登录信息失败');
|
||||
}
|
||||
|
||||
if (message.data.code !== 0) {
|
||||
switch (message.data.code) {
|
||||
case 86038:
|
||||
return { status: 'expired' };
|
||||
case 86090:
|
||||
return { status: 'scanned' };
|
||||
case 86101:
|
||||
return { status: 'waiting' };
|
||||
default:
|
||||
return { status: 'unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
const cookies = response.headers.get('set-cookie');
|
||||
if (!cookies) {
|
||||
throw new Error('无法获取 Cookie');
|
||||
}
|
||||
|
||||
return { status: 'confirmed', cookie: extractCookie(cookies), refresh_token: message.data.refresh_token };
|
||||
}
|
||||
|
||||
function extractCookie(cookies: string): string {
|
||||
const cookieArray = cookies
|
||||
.split(',')
|
||||
.map((cookie) => cookie.split(';')[0].trim())
|
||||
.filter(Boolean);
|
||||
const cookieSet = new Set(cookieArray);
|
||||
return Array.from(cookieSet).join('; ');
|
||||
}
|
||||
236
src/client/data/info.ts
Normal file
236
src/client/data/info.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { ref } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
import { info, error } from '@tauri-apps/plugin-log';
|
||||
import { QueryBiliAPI } from './utils'; // 假设 Bili API 工具路径
|
||||
import { BiliRoomInfo, BiliStreamingInfo, FetcherStatisticData } from './models'; // 假设模型路径
|
||||
import { useTauriStore } from '../store/useTauriStore';
|
||||
// import { useAccount } from '@/api/account'; // 如果需要账户信息
|
||||
|
||||
// const accountInfo = useAccount(); // 如果需要
|
||||
|
||||
export const STATISTIC_STORE_KEY = 'webfetcher.statistics';
|
||||
|
||||
/**
|
||||
* 当前日期 (YYYY-MM-DD) 的统计数据 (会被持久化)
|
||||
*/
|
||||
export const currentStatistic = ref<FetcherStatisticData>();
|
||||
/**
|
||||
* 标记当前统计数据是否已更新且需要保存
|
||||
*/
|
||||
export const shouldUpdateStatistic = ref(false);
|
||||
|
||||
/**
|
||||
* 直播流信息 (从B站API获取)
|
||||
*/
|
||||
export const streamingInfo = ref<BiliStreamingInfo>({
|
||||
status: 'prepare', // 初始状态
|
||||
} as BiliStreamingInfo);
|
||||
|
||||
/**
|
||||
* 房间基本信息 (从B站API获取)
|
||||
*/
|
||||
export const roomInfo = ref<BiliRoomInfo>(); // 可以添加房间信息
|
||||
|
||||
// --- Bili API 更新相关 ---
|
||||
const updateCount = ref(0); // 用于控制API调用频率的计数器
|
||||
|
||||
/**
|
||||
* 初始化统计和信息获取逻辑
|
||||
*/
|
||||
export function initInfo() {
|
||||
// 立即执行一次以加载或初始化当天数据
|
||||
updateCallback();
|
||||
// 设置定时器,定期检查和保存统计数据,并更新直播间信息
|
||||
setInterval(() => {
|
||||
updateCallback();
|
||||
}, 5000); // 每 5 秒检查一次统计数据保存和更新直播信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时回调函数: 处理统计数据持久化和B站信息更新
|
||||
*/
|
||||
async function updateCallback() {
|
||||
const store = useTauriStore();
|
||||
const currentDate = format(new Date(), 'yyyy-MM-dd');
|
||||
const key = `${STATISTIC_STORE_KEY}.${currentDate}`;
|
||||
|
||||
// --- 统计数据管理 ---
|
||||
// 检查是否需要加载或初始化当天的统计数据
|
||||
if (!currentStatistic.value || currentStatistic.value.date !== currentDate) {
|
||||
const loadedData = await store.get<FetcherStatisticData>(key);
|
||||
if (loadedData && loadedData.date === currentDate) {
|
||||
currentStatistic.value = loadedData;
|
||||
// 确保 eventTypeCounts 存在
|
||||
if (!currentStatistic.value.eventTypeCounts) {
|
||||
currentStatistic.value.eventTypeCounts = {};
|
||||
}
|
||||
// info(`Loaded statistics for ${currentDate}`); // 日志保持不变
|
||||
} else {
|
||||
info(`Initializing statistics for new day: ${currentDate}`);
|
||||
currentStatistic.value = {
|
||||
date: currentDate,
|
||||
count: 0,
|
||||
eventTypeCounts: {}, // 初始化类型计数
|
||||
};
|
||||
await store.set(key, currentStatistic.value); // 立即保存新一天的初始结构
|
||||
shouldUpdateStatistic.value = false; // 重置保存标记
|
||||
|
||||
// 清理旧数据逻辑 (保持不变)
|
||||
const allKeys = (await store.store.keys()).filter((k) => k.startsWith(STATISTIC_STORE_KEY));
|
||||
if (allKeys.length > 30) { // 例如,只保留最近30天的数据
|
||||
allKeys.sort(); // 按日期字符串升序排序
|
||||
const oldestKey = allKeys[0];
|
||||
await store.store.delete(oldestKey);
|
||||
info('清理过期统计数据: ' + oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果数据有更新,则保存
|
||||
if (shouldUpdateStatistic.value && currentStatistic.value) {
|
||||
try {
|
||||
await store.set(key, currentStatistic.value);
|
||||
shouldUpdateStatistic.value = false; // 保存后重置标记
|
||||
} catch (err) {
|
||||
error("Failed to save statistics: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- B站信息更新 ---
|
||||
let updateDelay = 30; // 默认30秒更新一次房间信息
|
||||
if (streamingInfo.value.status === 'streaming' && !import.meta.env.DEV) {
|
||||
updateDelay = 15; // 直播中15秒更新一次 (可以适当调整)
|
||||
}
|
||||
// 使用取模运算控制调用频率
|
||||
if (updateCount.value % (updateDelay / 5) === 0) { // 因为主循环是5秒一次
|
||||
updateRoomAndStreamingInfo();
|
||||
}
|
||||
updateCount.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一个接收到的事件 (由 useWebFetcher 调用)
|
||||
* @param eventType 事件类型字符串 (例如 "DANMU_MSG")
|
||||
*/
|
||||
export function recordEvent(eventType: string) {
|
||||
const currentDate = format(new Date(), 'yyyy-MM-dd');
|
||||
|
||||
// 确保 currentStatistic 已为当天初始化
|
||||
if (!currentStatistic.value || currentStatistic.value.date !== currentDate) {
|
||||
// 理论上 updateCallback 会先执行初始化,这里加个警告以防万一
|
||||
console.warn("recordEvent called before currentStatistic was initialized for today.");
|
||||
// 可以选择在这里强制调用一次 updateCallback 来初始化,但这可能是异步的
|
||||
// await updateCallback(); // 可能会引入复杂性
|
||||
return; // 或者直接返回,丢失这个事件计数
|
||||
}
|
||||
|
||||
// 增加总数
|
||||
currentStatistic.value.count++;
|
||||
|
||||
// 增加对应类型的计数
|
||||
if (!currentStatistic.value.eventTypeCounts) {
|
||||
currentStatistic.value.eventTypeCounts = {}; // 防御性初始化
|
||||
}
|
||||
currentStatistic.value.eventTypeCounts[eventType] = (currentStatistic.value.eventTypeCounts[eventType] || 0) + 1;
|
||||
|
||||
// 标记需要保存
|
||||
shouldUpdateStatistic.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 command 数据中解析事件类型
|
||||
* (需要根据实际接收到的数据结构调整)
|
||||
*/
|
||||
export function getEventType(command: any): string {
|
||||
if (typeof command === 'string') {
|
||||
try {
|
||||
command = JSON.parse(command);
|
||||
} catch (e) {
|
||||
return 'UNKNOWN_FORMAT';
|
||||
}
|
||||
}
|
||||
|
||||
if (command && typeof command === 'object') {
|
||||
// 优先使用 'cmd' 字段 (常见于 Web 或 OpenLive)
|
||||
if (command.cmd) return command.cmd;
|
||||
// 备选 'command' 字段
|
||||
if (command.command) return command.command;
|
||||
// 备选 'type' 字段
|
||||
if (command.type) return command.type;
|
||||
}
|
||||
return 'UNKNOWN'; // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定天数的历史统计数据
|
||||
* @param days 要获取的天数,默认为 7
|
||||
*/
|
||||
export async function getHistoricalStatistics(days: number = 7): Promise<FetcherStatisticData[]> {
|
||||
const store = useTauriStore();
|
||||
const keys = (await store.store.keys())
|
||||
.filter(key => key.startsWith(STATISTIC_STORE_KEY))
|
||||
.sort((a, b) => b.localeCompare(a)); // 按日期降序排序
|
||||
|
||||
const historicalData: FetcherStatisticData[] = [];
|
||||
for (let i = 0; i < Math.min(days, keys.length); i++) {
|
||||
const data = await store.get<FetcherStatisticData>(keys[i]);
|
||||
if (data) {
|
||||
historicalData.push(data);
|
||||
}
|
||||
}
|
||||
return historicalData.reverse(); // 返回按日期升序排列的结果
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新房间和直播流信息
|
||||
*/
|
||||
async function updateRoomAndStreamingInfo() {
|
||||
// 需要一个房间ID来查询,这个ID可能来自设置、登录信息或固定配置
|
||||
// const roomId = accountInfo.value?.roomid ?? settings.value.roomId; // 示例:获取房间ID
|
||||
const roomId = 21484828; // !!! 示例:这里需要替换成实际获取房间ID的逻辑 !!!
|
||||
if (!roomId) {
|
||||
// error("无法获取房间ID以更新直播信息");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询房间基本信息
|
||||
const roomRes = await QueryBiliAPI(
|
||||
`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`
|
||||
);
|
||||
const json = await roomRes.json();
|
||||
if (json.code === 0) {
|
||||
roomInfo.value = json.data;
|
||||
} else {
|
||||
error(`Failed to fetch Bili room info: ${json.message}`);
|
||||
}
|
||||
// 查询直播流信息 (开放平台或Web接口)
|
||||
// 注意:这里可能需要根据所选模式(openlive/direct)调用不同的API
|
||||
// 以下是Web接口示例
|
||||
const streamRes = await QueryBiliAPI(
|
||||
`https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids?uids[]=${roomInfo.value?.uid}` // 通过 UID 查询
|
||||
// 或者使用 `https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?room_ids=${roomId}&req_biz=web_room_componet`
|
||||
);
|
||||
const streamJson = await streamRes.json();
|
||||
if (streamJson.code === 0 && streamJson.data && roomInfo.value?.uid) {
|
||||
// Web API 返回的是一个以 UID 为 key 的对象
|
||||
const uidData = streamJson.data[roomInfo.value.uid.toString()];
|
||||
if (uidData) {
|
||||
streamingInfo.value = {
|
||||
...uidData, // 合并获取到的数据
|
||||
status: uidData.live_status === 1 ? 'streaming' : uidData.live_status === 2 ? 'rotating' : 'prepare',
|
||||
};
|
||||
} else {
|
||||
// 如果没有对应UID的数据,可能表示未开播或接口变更
|
||||
//streamingInfo.value = { status: 'prepare', ...streamingInfo.value }; // 保留旧数据,状态设为prepare
|
||||
}
|
||||
} else if (streamJson.code !== 0) {
|
||||
error(`Failed to fetch Bili streaming info: ${streamJson.message}`);
|
||||
// 可选:如果获取失败,将状态设为未知或准备
|
||||
// streamingInfo.value = { status: 'prepare', ...streamingInfo.value };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error("Error updating room/streaming info: " + err);
|
||||
}
|
||||
}
|
||||
243
src/client/data/initialize.ts
Normal file
243
src/client/data/initialize.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { isLoggedIn, useAccount } from "@/api/account";
|
||||
import { attachConsole, info, warn } from "@tauri-apps/plugin-log";
|
||||
import { useSettings } from "../store/useSettings";
|
||||
import { useWebFetcher } from "@/store/useWebFetcher";
|
||||
import { useBiliCookie } from "../store/useBiliCookie";
|
||||
import { getBuvid, getRoomKey } from "./utils";
|
||||
import { initInfo } from "./info";
|
||||
import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray';
|
||||
import { Menu } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
onAction,
|
||||
requestPermission,
|
||||
sendNotification,
|
||||
} from '@tauri-apps/plugin-notification';
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { CN_HOST } from "@/data/constants";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
|
||||
const accountInfo = useAccount();
|
||||
|
||||
export const clientInited = ref(false);
|
||||
let tray: TrayIcon;
|
||||
export async function initAll() {
|
||||
if (clientInited.value) {
|
||||
return;
|
||||
}
|
||||
let permissionGranted = await isPermissionGranted();
|
||||
|
||||
// If not we need to request it
|
||||
if (!permissionGranted) {
|
||||
const permission = await requestPermission();
|
||||
permissionGranted = permission === 'granted';
|
||||
if (permissionGranted) {
|
||||
info('Notification permission granted');
|
||||
}
|
||||
|
||||
}
|
||||
initNotificationHandler();
|
||||
const detach = await attachConsole();
|
||||
const settings = useSettings();
|
||||
const biliCookie = useBiliCookie();
|
||||
await settings.init();
|
||||
info('[init] 已加载账户信息');
|
||||
biliCookie.init();
|
||||
info('[init] 已加载bilibili cookie');
|
||||
initInfo();
|
||||
info('[init] 开始更新数据');
|
||||
|
||||
if (isLoggedIn && accountInfo.value.isBiliVerified) {
|
||||
const danmakuInitNoticeRef = window.$notification.info({
|
||||
title: '正在初始化弹幕客户端...',
|
||||
closable: false
|
||||
});
|
||||
const result = await initDanmakuClient();
|
||||
danmakuInitNoticeRef.destroy();
|
||||
if (result.success) {
|
||||
window.$notification.success({
|
||||
title: '弹幕客户端初始化完成',
|
||||
duration: 3000
|
||||
});
|
||||
} else {
|
||||
window.$notification.error({
|
||||
title: '弹幕客户端初始化失败: ' + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
info('[init] 已加载弹幕客户端');
|
||||
// 初始化系统托盘图标和菜单
|
||||
const menu = await Menu.new({
|
||||
items: [
|
||||
{
|
||||
id: 'quit',
|
||||
text: '退出',
|
||||
action: () => {
|
||||
invoke('quit_app');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer();
|
||||
const appWindow = getCurrentWindow();
|
||||
const options: TrayIconOptions = {
|
||||
// here you can add a tray menu, title, tooltip, event handler, etc
|
||||
menu: menu,
|
||||
title: 'VTsuru.Client',
|
||||
tooltip: 'VTsuru 事件收集器',
|
||||
icon: iconData,
|
||||
action: (event) => {
|
||||
|
||||
switch (event.type) {
|
||||
case 'DoubleClick':
|
||||
appWindow.show();
|
||||
break;
|
||||
case 'Click':
|
||||
appWindow.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
tray = await TrayIcon.new(options);
|
||||
|
||||
clientInited.value = true;
|
||||
}
|
||||
export function OnClientUnmounted() {
|
||||
if (clientInited.value) {
|
||||
clientInited.value = false;
|
||||
}
|
||||
|
||||
tray.close();
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
const update = await check();
|
||||
if (update) {
|
||||
console.log(
|
||||
`found update ${update.version} from ${update.date} with notes ${update.body}`
|
||||
);
|
||||
let downloaded = 0;
|
||||
let contentLength = 0;
|
||||
// alternatively we could also call update.download() and update.install() separately
|
||||
await update.downloadAndInstall((event) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
contentLength = event.data.contentLength || 0;
|
||||
console.log(`started downloading ${event.data.contentLength} bytes`);
|
||||
break;
|
||||
case 'Progress':
|
||||
downloaded += event.data.chunkLength;
|
||||
console.log(`downloaded ${downloaded} from ${contentLength}`);
|
||||
break;
|
||||
case 'Finished':
|
||||
console.log('download finished');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('update installed');
|
||||
await relaunch();
|
||||
}
|
||||
}
|
||||
|
||||
export const isInitedDanmakuClient = ref(false);
|
||||
export const isInitingDanmakuClient = ref(false);
|
||||
export async function initDanmakuClient() {
|
||||
const biliCookie = useBiliCookie();
|
||||
const settings = useSettings();
|
||||
if (isInitedDanmakuClient.value || isInitingDanmakuClient.value) {
|
||||
return { success: true, message: '' };
|
||||
}
|
||||
isInitingDanmakuClient.value = true;
|
||||
let result = { success: false, message: '' };
|
||||
try {
|
||||
if (isLoggedIn) {
|
||||
if (settings.settings.useDanmakuClientType === 'openlive') {
|
||||
result = await initOpenLive();
|
||||
} else {
|
||||
const cookie = await biliCookie.getBiliCookie();
|
||||
if (!cookie) {
|
||||
if (settings.settings.fallbackToOpenLive) {
|
||||
settings.settings.useDanmakuClientType = 'openlive';
|
||||
settings.save();
|
||||
info('未设置bilibili cookie, 根据设置切换为openlive');
|
||||
result = await initOpenLive();
|
||||
} else {
|
||||
info('未设置bilibili cookie, 跳过弹幕客户端初始化');
|
||||
window.$notification.warning({
|
||||
title: '未设置bilibili cookie, 跳过弹幕客户端初始化',
|
||||
duration: 5,
|
||||
});
|
||||
result = { success: false, message: '未设置bilibili cookie' };
|
||||
}
|
||||
} else {
|
||||
const resp = await callStartDanmakuClient();
|
||||
if (!resp?.success) {
|
||||
warn('加载弹幕客户端失败: ' + resp?.message);
|
||||
result = { success: false, message: resp?.message };
|
||||
} else {
|
||||
info('已加载弹幕客户端');
|
||||
result = { success: true, message: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
warn('加载弹幕客户端失败: ' + err);
|
||||
return { success: false, message: '加载弹幕客户端失败' };
|
||||
} finally {
|
||||
if (result) {
|
||||
isInitedDanmakuClient.value = true;
|
||||
}
|
||||
isInitingDanmakuClient.value = false;
|
||||
}
|
||||
}
|
||||
export async function initOpenLive() {
|
||||
const reuslt = await callStartDanmakuClient();
|
||||
if (reuslt?.success == true) {
|
||||
info('已加载弹幕客户端 [openlive]');
|
||||
} else {
|
||||
warn('加载弹幕客户端失败 [openlive]: ' + reuslt?.message);
|
||||
}
|
||||
return reuslt;
|
||||
}
|
||||
function initNotificationHandler(){
|
||||
onAction((event) => {
|
||||
if (event.extra?.type === 'question-box') {
|
||||
openUrl(CN_HOST + '/manage/question-box');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function callStartDanmakuClient() {
|
||||
const biliCookie = useBiliCookie();
|
||||
const settings = useSettings();
|
||||
const webFetcher = useWebFetcher();
|
||||
if (settings.settings.useDanmakuClientType === 'direct') {
|
||||
const key = await getRoomKey(
|
||||
accountInfo.value.biliRoomId!, await biliCookie.getBiliCookie() || '');
|
||||
if (!key) {
|
||||
warn('获取房间密钥失败, 无法连接弹幕客户端');
|
||||
return { success: false, message: '无法获取房间密钥' };
|
||||
}
|
||||
const buvid = await getBuvid();
|
||||
if (!buvid) {
|
||||
warn('获取buvid失败, 无法连接弹幕客户端');
|
||||
return { success: false, message: '无法获取buvid' };
|
||||
}
|
||||
return await webFetcher.Start('direct', {
|
||||
roomId: accountInfo.value.biliRoomId!,
|
||||
buvid: buvid.data,
|
||||
token: key,
|
||||
tokenUserId: biliCookie.uId!,
|
||||
}, true);
|
||||
} else {
|
||||
return await webFetcher.Start('openlive', undefined, true);
|
||||
}
|
||||
}
|
||||
282
src/client/data/models.ts
Normal file
282
src/client/data/models.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
export interface EventFetcherStateModel {
|
||||
online: boolean;
|
||||
status: { [errorCode: string]: string };
|
||||
version?: string;
|
||||
todayReceive: number;
|
||||
useCookie: boolean;
|
||||
type: EventFetcherType;
|
||||
}
|
||||
|
||||
export enum EventFetcherType {
|
||||
Application,
|
||||
OBS,
|
||||
Server,
|
||||
Tauri,
|
||||
}
|
||||
|
||||
export type BiliRoomInfo = {
|
||||
uid: number;
|
||||
room_id: number;
|
||||
short_id: number;
|
||||
attention: number;
|
||||
online: number;
|
||||
is_portrait: boolean;
|
||||
description: string;
|
||||
live_status: number;
|
||||
area_id: number;
|
||||
parent_area_id: number;
|
||||
parent_area_name: string;
|
||||
old_area_id: number;
|
||||
background: string;
|
||||
title: string;
|
||||
user_cover: string;
|
||||
keyframe: string;
|
||||
is_strict_room: boolean;
|
||||
live_time: string;
|
||||
tags: string;
|
||||
is_anchor: number;
|
||||
room_silent_type: string;
|
||||
room_silent_level: number;
|
||||
room_silent_second: number;
|
||||
area_name: string;
|
||||
pendants: string;
|
||||
area_pendants: string;
|
||||
hot_words: string[];
|
||||
hot_words_status: number;
|
||||
verify: string;
|
||||
new_pendants: {
|
||||
frame: {
|
||||
name: string;
|
||||
value: string;
|
||||
position: number;
|
||||
desc: string;
|
||||
area: number;
|
||||
area_old: number;
|
||||
bg_color: string;
|
||||
bg_pic: string;
|
||||
use_old_area: boolean;
|
||||
};
|
||||
badge: unknown; // null in the example, adjust to proper type if known
|
||||
mobile_frame: {
|
||||
name: string;
|
||||
value: string;
|
||||
position: number;
|
||||
desc: string;
|
||||
area: number;
|
||||
area_old: number;
|
||||
bg_color: string;
|
||||
bg_pic: string;
|
||||
use_old_area: boolean;
|
||||
};
|
||||
mobile_badge: unknown; // null in the example, adjust to proper type if known
|
||||
};
|
||||
up_session: string;
|
||||
pk_status: number;
|
||||
pk_id: number;
|
||||
battle_id: number;
|
||||
allow_change_area_time: number;
|
||||
allow_upload_cover_time: number;
|
||||
studio_info: {
|
||||
status: number;
|
||||
master_list: any[]; // empty array in the example, adjust to proper type if known
|
||||
};
|
||||
}
|
||||
|
||||
export type FetcherStatisticData = {
|
||||
date: string;
|
||||
count: number;
|
||||
eventTypeCounts: { [eventType: string]: number };
|
||||
};
|
||||
export type BiliStreamingInfo = {
|
||||
status: 'prepare' | 'streaming' | 'cycle';
|
||||
streamAt: Date;
|
||||
roomId: number;
|
||||
title: string;
|
||||
coverUrl: string;
|
||||
frameUrl: string;
|
||||
areaName: string;
|
||||
parentAreaName: string;
|
||||
online: number;
|
||||
attention: number;
|
||||
};
|
||||
|
||||
// Nested type for Vip Label
|
||||
interface VipLabel {
|
||||
path: string;
|
||||
text: string;
|
||||
label_theme: string;
|
||||
text_color: string;
|
||||
bg_style: number;
|
||||
bg_color: string;
|
||||
border_color: string;
|
||||
use_img_label: boolean;
|
||||
img_label_uri_hans: string;
|
||||
img_label_uri_hant: string;
|
||||
img_label_uri_hans_static: string;
|
||||
img_label_uri_hant_static: string;
|
||||
}
|
||||
|
||||
// Nested type for Avatar Icon
|
||||
interface AvatarIcon {
|
||||
icon_type: number;
|
||||
// Assuming icon_resource could contain arbitrary data or be empty
|
||||
icon_resource: Record<string, unknown> | {};
|
||||
}
|
||||
|
||||
// Nested type for Vip Info
|
||||
interface VipInfo {
|
||||
type: number;
|
||||
status: number;
|
||||
due_date: number; // Likely a Unix timestamp in milliseconds
|
||||
vip_pay_type: number;
|
||||
theme_type: number;
|
||||
label: VipLabel;
|
||||
avatar_subscript: number;
|
||||
nickname_color: string;
|
||||
role: number;
|
||||
avatar_subscript_url: string;
|
||||
tv_vip_status: number;
|
||||
tv_vip_pay_type: number;
|
||||
tv_due_date: number; // Likely a Unix timestamp in milliseconds or 0
|
||||
avatar_icon: AvatarIcon;
|
||||
}
|
||||
|
||||
// Nested type for Pendant Info
|
||||
interface PendantInfo {
|
||||
pid: number;
|
||||
name: string;
|
||||
image: string; // URL
|
||||
expire: number; // Likely a timestamp or duration
|
||||
image_enhance: string; // URL
|
||||
image_enhance_frame: string; // URL or empty string
|
||||
n_pid: number;
|
||||
}
|
||||
|
||||
// Nested type for Nameplate Info
|
||||
interface NameplateInfo {
|
||||
nid: number;
|
||||
name: string;
|
||||
image: string; // URL
|
||||
image_small: string; // URL
|
||||
level: string;
|
||||
condition: string;
|
||||
}
|
||||
|
||||
// Nested type for Official Info
|
||||
interface OfficialInfo {
|
||||
role: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
// Nested type for Profession Info
|
||||
interface ProfessionInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
show_name: string;
|
||||
is_show: number; // Likely 0 or 1
|
||||
category_one: string;
|
||||
realname: string;
|
||||
title: string;
|
||||
department: string;
|
||||
certificate_no: string;
|
||||
certificate_show: boolean;
|
||||
}
|
||||
|
||||
// Nested type for Honours Colour
|
||||
interface HonoursColour {
|
||||
dark: string; // Hex color code
|
||||
normal: string; // Hex color code
|
||||
}
|
||||
|
||||
// Nested type for Honours Info
|
||||
interface HonoursInfo {
|
||||
mid: number;
|
||||
colour: HonoursColour;
|
||||
// Assuming tags could be an array of strings if not null
|
||||
tags: string[] | null;
|
||||
is_latest_100honour: number; // Likely 0 or 1
|
||||
}
|
||||
|
||||
// Nested type for Attestation Common Info
|
||||
interface CommonAttestationInfo {
|
||||
title: string;
|
||||
prefix: string;
|
||||
prefix_title: string;
|
||||
}
|
||||
|
||||
// Nested type for Attestation Splice Info
|
||||
interface SpliceAttestationInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Nested type for Attestation Info
|
||||
interface AttestationInfo {
|
||||
type: number;
|
||||
common_info: CommonAttestationInfo;
|
||||
splice_info: SpliceAttestationInfo;
|
||||
icon: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
// Nested type for Expert Info
|
||||
interface ExpertInfo {
|
||||
title: string;
|
||||
state: number;
|
||||
type: number;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
// Nested type for Level Exp Info
|
||||
interface LevelExpInfo {
|
||||
current_level: number;
|
||||
current_min: number;
|
||||
current_exp: number;
|
||||
next_exp: number; // -1 might indicate max level or data not applicable
|
||||
level_up: number; // Likely a Unix timestamp
|
||||
}
|
||||
|
||||
// Main User Profile Type
|
||||
export type BiliUserProfile = {
|
||||
mid: number;
|
||||
name: string;
|
||||
sex: string; // Could be more specific like '男' | '女' | '保密' if desired
|
||||
face: string; // URL
|
||||
sign: string;
|
||||
rank: number;
|
||||
level: number;
|
||||
jointime: number; // Likely a Unix timestamp or 0
|
||||
moral: number;
|
||||
silence: number; // Likely 0 or 1
|
||||
email_status: number; // Likely 0 or 1
|
||||
tel_status: number; // Likely 0 or 1
|
||||
identification: number; // Likely 0 or 1
|
||||
vip: VipInfo;
|
||||
pendant: PendantInfo;
|
||||
nameplate: NameplateInfo;
|
||||
official: OfficialInfo;
|
||||
birthday: number; // Likely a Unix timestamp
|
||||
is_tourist: number; // Likely 0 or 1
|
||||
is_fake_account: number; // Likely 0 or 1
|
||||
pin_prompting: number; // Likely 0 or 1
|
||||
is_deleted: number; // Likely 0 or 1
|
||||
in_reg_audit: number; // Likely 0 or 1
|
||||
is_rip_user: boolean;
|
||||
profession: ProfessionInfo;
|
||||
face_nft: number;
|
||||
face_nft_new: number;
|
||||
is_senior_member: number; // Likely 0 or 1
|
||||
honours: HonoursInfo;
|
||||
digital_id: string;
|
||||
digital_type: number;
|
||||
attestation: AttestationInfo;
|
||||
expert_info: ExpertInfo;
|
||||
// Assuming name_render could be various types or null
|
||||
name_render: any | null;
|
||||
country_code: string;
|
||||
level_exp: LevelExpInfo;
|
||||
coins: number; // Can be float
|
||||
following: number;
|
||||
follower: number;
|
||||
};
|
||||
27
src/client/data/notification.ts
Normal file
27
src/client/data/notification.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { QAInfo } from "@/api/api-models";
|
||||
import { useSettings } from "../store/useSettings";
|
||||
import { isPermissionGranted, onAction, sendNotification } from "@tauri-apps/plugin-notification";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { CN_HOST } from "@/data/constants";
|
||||
|
||||
export async function onReceivedQuestion(question: QAInfo) {
|
||||
const setting = useSettings();
|
||||
if (setting.settings.notificationSettings.enableTypes.includes("question-box")) {
|
||||
window.$notification.info({
|
||||
title: "收到提问",
|
||||
description: '收到来自 [' + question.sender.name || '匿名用户' + '] 的提问',
|
||||
duration: 5,
|
||||
});
|
||||
let permissionGranted = await isPermissionGranted();
|
||||
if (permissionGranted) {
|
||||
sendNotification({
|
||||
title: "收到提问",
|
||||
body: '来自 [' + question.sender.name || '匿名用户' + '] 的提问',
|
||||
silent: false,
|
||||
extra: { type: 'question-box' },
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
77
src/client/data/utils.ts
Normal file
77
src/client/data/utils.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { useBiliCookie } from '../store/useBiliCookie';
|
||||
import { QueryPostAPI } from '@/api/query';
|
||||
import { OPEN_LIVE_API_URL } from '@/data/constants';
|
||||
import { error } from '@tauri-apps/plugin-log';
|
||||
|
||||
export async function QueryBiliAPI(url: string, method: string = 'GET', cookie: string = '') {
|
||||
const u = new URL(url);
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
|
||||
Origin: '',
|
||||
Cookie: cookie || (await useBiliCookie().getBiliCookie()) || '',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRoomKey(roomId: number, cookie: string) {
|
||||
try {
|
||||
const result = await QueryBiliAPI(
|
||||
'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=' + roomId
|
||||
);
|
||||
const json = await result.json();
|
||||
if (json.code === 0) return json.data.token;
|
||||
else {
|
||||
error(`无法获取直播间key: ${json.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
error(`无法获取直播间key: ${err}`);
|
||||
}
|
||||
}
|
||||
export async function getBuvid() {
|
||||
try {
|
||||
const result = await QueryBiliAPI('https://api.bilibili.com/x/web-frontend/getbuvid');
|
||||
if (result.ok) {
|
||||
const json = await result.json();
|
||||
if (json.code === 0) return json.data.buvid;
|
||||
else {
|
||||
error(`无法获取buvid: ${json.message}`);
|
||||
}
|
||||
} else {
|
||||
error(`无法获取buvid: ${result.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
error(`无法获取buvid: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAuthInfo(): Promise<{
|
||||
data: any;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
const data = await QueryPostAPI<any>(OPEN_LIVE_API_URL + 'start');
|
||||
if (data.code == 200) {
|
||||
console.log(`[open-live] 已获取认证信息`);
|
||||
return {
|
||||
data: data.data,
|
||||
message: '',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
data: null,
|
||||
message: data.message,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
data: null,
|
||||
message: err?.toString() || '未知错误',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user