mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
add web-fetcher
This commit is contained in:
@@ -7,7 +7,7 @@ import { ref } from 'vue'
|
||||
import { APIRoot, AccountInfo, FunctionTypes } from './api-models'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export const ACCOUNT = ref<AccountInfo>()
|
||||
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
|
||||
export const isLoadingAccount = ref(true)
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -30,6 +30,20 @@ export interface UserInfo {
|
||||
streamerInfo?: StreamerModel
|
||||
}
|
||||
}
|
||||
export interface EventFetcherStateModel {
|
||||
online: boolean
|
||||
status: { [errorCode: string]: string }
|
||||
version?: string
|
||||
todayReceive: number
|
||||
useCookie: boolean
|
||||
type: EventFetcherType
|
||||
}
|
||||
|
||||
export enum EventFetcherType {
|
||||
Application,
|
||||
OBS,
|
||||
Server,
|
||||
}
|
||||
export interface AccountInfo extends UserInfo {
|
||||
isEmailVerified: boolean
|
||||
isBiliVerified: boolean
|
||||
@@ -41,11 +55,7 @@ export interface AccountInfo extends UserInfo {
|
||||
biliAuthCode?: string
|
||||
biliAuthCodeStatus: BiliAuthCodeStatusType
|
||||
|
||||
eventFetcherOnline: boolean
|
||||
eventFetcherStatus: string
|
||||
eventFetcherStatusV3: { [errorCode: string]: string }
|
||||
eventFetcherTodayReceive: number
|
||||
eventFetcherVersion?: string
|
||||
eventFetcherState: EventFetcherStateModel
|
||||
|
||||
nextSendEmailTime?: number
|
||||
isServerFetcherOnline: boolean
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function QueryPostAPIWithParams<T>(
|
||||
const url = new URL(urlString)
|
||||
url.search = getParams(params)
|
||||
headers ??= []
|
||||
headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
|
||||
if (contentType) headers?.push(['Content-Type', contentType])
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function QueryGetAPI<T>(
|
||||
url.search = getParams(params)
|
||||
if (cookie.value) {
|
||||
headers ??= []
|
||||
headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
}
|
||||
try {
|
||||
const data = await fetch(url.toString(), {
|
||||
@@ -81,6 +81,9 @@ function getParams(params?: [string, string][]) {
|
||||
if (urlParams.has('as')) {
|
||||
resultParams.set('as', urlParams.get('as') || '')
|
||||
}
|
||||
if (urlParams.has('token')) {
|
||||
resultParams.set('token', urlParams.get('token') || '')
|
||||
}
|
||||
return resultParams.toString()
|
||||
}
|
||||
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account'
|
||||
import { EventFetcherType } from '@/api/api-models'
|
||||
import { FlashCheckmark16Filled, Info24Filled } from '@vicons/fluent'
|
||||
import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const state = accountInfo.value?.eventFetcherState
|
||||
|
||||
const status = computed(() => {
|
||||
if (!accountInfo.value) return 'error'
|
||||
if (accountInfo.value.eventFetcherOnline == true || accountInfo.value.isServerFetcherOnline == true) {
|
||||
if (accountInfo.value.eventFetcherStatus) {
|
||||
if (state.online == true) {
|
||||
if (state.status == undefined || state.status == null) {
|
||||
return 'warning'
|
||||
} else if (Object.keys(accountInfo.value.eventFetcherStatusV3 ?? {}).length > 0) {
|
||||
} else if (Object.keys(state.status ?? {}).length > 0) {
|
||||
return 'warning'
|
||||
} else {
|
||||
return 'success'
|
||||
@@ -23,7 +24,7 @@ const status = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert :type="status">
|
||||
<NAlert :type="status" v-if="status">
|
||||
<template #header>
|
||||
EVENT-FETCHER 状态
|
||||
<NTooltip>
|
||||
@@ -50,7 +51,7 @@ const status = computed(() => {
|
||||
<template #trigger>
|
||||
<NTag size="small" :color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }">
|
||||
<NIcon :component="FlashCheckmark16Filled" />
|
||||
{{ accountInfo?.eventFetcherVersion ?? '未知' }}
|
||||
{{ state.type == EventFetcherType.OBS ? 'OBS/网页端' : state.version ?? '未知' }}
|
||||
</NTag>
|
||||
</template>
|
||||
你所使用的版本
|
||||
@@ -58,7 +59,7 @@ const status = computed(() => {
|
||||
<NDivider vertical />
|
||||
</template>
|
||||
<NTag :type="status">
|
||||
<template v-if="accountInfo?.eventFetcherOnline == true && accountInfo?.eventFetcherStatus">
|
||||
<template v-if="state?.online == true && (state?.status == null || state?.status == undefined)">
|
||||
此版本已过期, 请更新
|
||||
<NTooltip trigger="click">
|
||||
<template #trigger>
|
||||
@@ -75,19 +76,17 @@ const status = computed(() => {
|
||||
</NText>
|
||||
| 今日已接收
|
||||
<NText color="white" strong>
|
||||
{{ accountInfo?.eventFetcherTodayReceive }}
|
||||
{{ state.todayReceive }}
|
||||
</NText>
|
||||
条
|
||||
</template>
|
||||
<template v-else-if="status == 'warning'">
|
||||
<template v-if="accountInfo?.eventFetcherStatusV3">
|
||||
异常: {{ Object.values(accountInfo.eventFetcherStatusV3).join('; ') }}
|
||||
</template>
|
||||
<template v-if="state.status"> 异常: {{ Object.values(state.status).join('; ') }} </template>
|
||||
</template>
|
||||
<template v-else-if="status == 'info'"> 未连接 </template>
|
||||
</template>
|
||||
</NTag>
|
||||
<template v-if="accountInfo?.eventFetcherOnline != true">
|
||||
<template v-if="!state.online">
|
||||
<NDivider vertical />
|
||||
<NButton
|
||||
type="info"
|
||||
|
||||
@@ -159,6 +159,7 @@ interface DanmakuEventsMap {
|
||||
gift: (arg1: GiftInfo, arg2?: any) => void
|
||||
sc: (arg1: SCInfo, arg2?: any) => void
|
||||
guard: (arg1: GuardInfo, arg2?: any) => void
|
||||
all: (arg1: any) => void
|
||||
}
|
||||
|
||||
export default class DanmakuClient {
|
||||
@@ -167,54 +168,83 @@ export default class DanmakuClient {
|
||||
}
|
||||
|
||||
private client: any
|
||||
private authInfo: AuthInfo | null
|
||||
private timer: any | undefined
|
||||
private isStarting = false
|
||||
|
||||
public authInfo: AuthInfo | null
|
||||
public roomAuthInfo = ref<RoomAuthInfo>({} as RoomAuthInfo)
|
||||
public authCode: string | undefined
|
||||
|
||||
public isRunning: boolean = false
|
||||
|
||||
private events: {
|
||||
danmaku: ((arg1: DanmakuInfo, arg2?: any) => void)[]
|
||||
gift: ((arg1: GiftInfo, arg2?: any) => void)[]
|
||||
sc: ((arg1: SCInfo, arg2?: any) => void)[]
|
||||
guard: ((arg1: GuardInfo, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
} = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: [],
|
||||
}
|
||||
private eventsAsModel: {
|
||||
danmaku: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
gift: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
sc: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
guard: ((arg1: EventModel, arg2?: any) => void)[]
|
||||
all: ((arg1: any) => void)[]
|
||||
} = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: [],
|
||||
}
|
||||
|
||||
public async Start(): Promise<{ success: boolean; message: string }> {
|
||||
if (!this.client) {
|
||||
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.timer = setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
|
||||
if (this.isRunning) {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端已被启动过',
|
||||
message: '弹幕客户端已启动',
|
||||
}
|
||||
}
|
||||
if (this.isStarting) {
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端正在启动',
|
||||
}
|
||||
}
|
||||
this.isStarting = true
|
||||
try {
|
||||
if (!this.client) {
|
||||
console.log('[OPEN-LIVE] 正在启动弹幕客户端')
|
||||
const result = await this.initClient()
|
||||
if (result.success) {
|
||||
this.isRunning = true
|
||||
this.timer ??= setInterval(() => {
|
||||
this.sendHeartbeat()
|
||||
}, 20 * 1000)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
console.warn('[OPEN-LIVE] 弹幕客户端已被启动过')
|
||||
return {
|
||||
success: false,
|
||||
message: '弹幕客户端已被启动过',
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isStarting = false
|
||||
}
|
||||
}
|
||||
public Stop() {
|
||||
if (!this.isRunning) {
|
||||
return
|
||||
}
|
||||
this.isRunning = false
|
||||
if (this.client) {
|
||||
console.log('[OPEN-LIVE] 正在停止弹幕客户端')
|
||||
this.client.stop()
|
||||
@@ -223,35 +253,51 @@ export default class DanmakuClient {
|
||||
}
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
this.events = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: [],
|
||||
}
|
||||
this.eventsAsModel = {
|
||||
danmaku: [],
|
||||
gift: [],
|
||||
sc: [],
|
||||
guard: [],
|
||||
all: [],
|
||||
}
|
||||
}
|
||||
private sendHeartbeat() {
|
||||
if (this.client) {
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
|
||||
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
|
||||
query.then((data) => {
|
||||
if (data.code != 200) {
|
||||
console.error('[OPEN-LIVE] 心跳失败')
|
||||
this.client.stop()
|
||||
this.client = null
|
||||
this.initClient()
|
||||
}
|
||||
})
|
||||
if (!this.isRunning) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
return
|
||||
}
|
||||
const query = this.authInfo
|
||||
? QueryPostAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat', this.authInfo)
|
||||
: QueryGetAPI<OpenLiveInfo>(OPEN_LIVE_API_URL + 'heartbeat-internal')
|
||||
query.then((data) => {
|
||||
if (data.code != 200) {
|
||||
console.error('[OPEN-LIVE] 心跳失败, 将重新连接')
|
||||
this.client?.stop()
|
||||
this.client = null
|
||||
this.initClient()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private onRawMessage = (command: any) => {
|
||||
this.eventsAsModel.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
this.events.all?.forEach((d) => {
|
||||
d(command)
|
||||
})
|
||||
}
|
||||
|
||||
private onDanmaku = (command: any) => {
|
||||
const data = command.data as DanmakuInfo
|
||||
|
||||
@@ -378,7 +424,8 @@ export default class DanmakuClient {
|
||||
public onEvent(eventName: 'gift', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'sc', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'guard', listener: (arg1: EventModel, arg2?: any) => void): this
|
||||
public onEvent(eventName: 'danmaku' | 'gift' | 'sc' | 'guard', listener: (...args: any[]) => void): this {
|
||||
public onEvent(eventName: 'all', listener: (arg1: any) => void): this
|
||||
public onEvent(eventName: 'danmaku' | 'gift' | 'sc' | 'guard' | 'all', listener: (...args: any[]) => void): this {
|
||||
if (!this.eventsAsModel[eventName]) {
|
||||
this.eventsAsModel[eventName] = []
|
||||
}
|
||||
@@ -418,7 +465,7 @@ export default class DanmakuClient {
|
||||
message: '',
|
||||
}
|
||||
} else {
|
||||
console.log('[OPEN-LIVE] 无法开启场次')
|
||||
console.log('[OPEN-LIVE] 无法开启场次: ' + auth.message)
|
||||
return {
|
||||
success: false,
|
||||
message: auth.message,
|
||||
@@ -456,5 +503,6 @@ export default class DanmakuClient {
|
||||
LIVE_OPEN_PLATFORM_SEND_GIFT: this.onGift.bind(this),
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT: this.onSC.bind(this),
|
||||
LIVE_OPEN_PLATFORM_GUARD: this.onGuard.bind(this),
|
||||
RAW_MESSAGE: this.onRawMessage.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,13 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
}
|
||||
this.onDelSuperChat({ ids })
|
||||
}
|
||||
|
||||
rawMessageCallback(command) {
|
||||
if (!this.onRawMessage) {
|
||||
return
|
||||
}
|
||||
this.onRawMessage(command)
|
||||
}
|
||||
}
|
||||
|
||||
const CMD_CALLBACK_MAP = {
|
||||
@@ -142,4 +149,5 @@ const CMD_CALLBACK_MAP = {
|
||||
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT: ChatClientDirectOpenLive.prototype.superChatCallback,
|
||||
LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL: ChatClientDirectOpenLive.prototype.superChatDelCallback,
|
||||
RAW_MESSAGE: ChatClientDirectOpenLive.prototype.rawMessageCallback,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// @ts-nocheck
|
||||
import { BrotliDecode } from './brotli_decode'
|
||||
import { setInterval, clearInterval, setTimeout, clearTimeout } from 'worker-timers'
|
||||
|
||||
@@ -145,7 +147,7 @@ export default class ChatClientOfficialBase {
|
||||
this.sendAuth()
|
||||
this.heartbeatTimerId = setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
console.log('ws 已连接')
|
||||
//console.log('ws 已连接')
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
@@ -270,7 +272,10 @@ export default class ChatClientOfficialBase {
|
||||
// 没压缩过的直接反序列化
|
||||
if (body.length !== 0) {
|
||||
try {
|
||||
body = JSON.parse(textDecoder.decode(body))
|
||||
const text = textDecoder.decode(body)
|
||||
this.onRawMessage(text)
|
||||
this.CMD_CALLBACK_MAP['RAW_MESSAGE']?.call(this, text)
|
||||
body = JSON.parse(text)
|
||||
this.handlerCommand(body)
|
||||
} catch (e) {
|
||||
console.error('body=', body)
|
||||
@@ -299,6 +304,7 @@ export default class ChatClientOfficialBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
onRawMessage(command) {}
|
||||
|
||||
handlerCommand(command) {
|
||||
let cmd = command.cmd || ''
|
||||
|
||||
@@ -2,8 +2,8 @@ import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemp
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
const debugAPI = import.meta.env.VITE_DEBUG_API
|
||||
const releseAPI = `https://vtsuru.suki.club/api/`
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/api/`
|
||||
const releseAPI = `https://vtsuru.suki.club/`
|
||||
const failoverAPI = `https://failover-api.vtsuru.suki.club/`
|
||||
|
||||
export const isBackendUsable = ref(true)
|
||||
|
||||
@@ -13,32 +13,38 @@ export const IMGUR_URL = FILE_BASE_URL + '/imgur/'
|
||||
export const THINGS_URL = FILE_BASE_URL + '/things/'
|
||||
export const apiFail = ref(false)
|
||||
|
||||
export const BASE_API = () =>
|
||||
process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI
|
||||
export const BASE_API = {
|
||||
toString: () =>
|
||||
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'api/',
|
||||
}
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/'
|
||||
export const BASE_HUB_URL = {
|
||||
toString: () =>
|
||||
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'hub/',
|
||||
}
|
||||
|
||||
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
|
||||
|
||||
export const USER_API_URL = { toString: () => `${BASE_API()}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API()}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API()}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API()}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API()}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API()}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API()}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API()}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API()}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API()}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API()}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API()}song-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API()}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API()}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API()}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API()}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API()}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API()}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API()}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API()}bili-auth/` }
|
||||
export const USER_API_URL = { toString: () => `${BASE_API}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API}song-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API}bili-auth/` }
|
||||
|
||||
export const ScheduleTemplateMap = {
|
||||
'': {
|
||||
|
||||
@@ -19,7 +19,7 @@ let currentVersion: string
|
||||
let isHaveNewVersion = false
|
||||
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version')
|
||||
.then((version) => {
|
||||
if (version.code == 200) {
|
||||
currentVersion = version.data
|
||||
@@ -42,7 +42,7 @@ QueryGetAPI<string>(BASE_API() + 'vtsuru/version')
|
||||
if (isHaveNewVersion) {
|
||||
return
|
||||
}
|
||||
QueryGetAPI<string>(BASE_API() + 'vtsuru/version').then((keepCheckData) => {
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version').then((keepCheckData) => {
|
||||
if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) {
|
||||
isHaveNewVersion = true
|
||||
currentVersion = version.data
|
||||
|
||||
@@ -42,5 +42,13 @@ export default {
|
||||
title: '棉花糖展示',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'web-fetcher',
|
||||
name: 'obs-web-fetcher',
|
||||
component: () => import('@/views/obs/WebFetcherOBS.vue'),
|
||||
meta: {
|
||||
title: '弹幕收集器 (OBS版',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
150
src/store/useWebFetcher.ts
Normal file
150
src/store/useWebFetcher.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import DanmakuClient from '@/data/DanmakuClient'
|
||||
import { BASE_HUB_URL } from '@/data/constants'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import * as msgpack from '@microsoft/signalr-protocol-msgpack'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { format } from 'date-fns'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
const cookie = useLocalStorage('JWT_Token', '')
|
||||
const route = useRoute()
|
||||
|
||||
const client = new DanmakuClient(null)
|
||||
|
||||
const events: string[] = []
|
||||
const isStarted = ref(false)
|
||||
let timer: any
|
||||
let signalRClient: signalR.HubConnection | null = null
|
||||
let disconnectedByServer = false
|
||||
|
||||
async function Start() {
|
||||
if (isStarted.value) {
|
||||
return
|
||||
}
|
||||
while (!(await connectSignalR())) {
|
||||
console.log('[WEB-FETCHER] 连接失败, 5秒后重试')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
}
|
||||
}
|
||||
function Stop() {
|
||||
if (!isStarted.value) {
|
||||
return
|
||||
}
|
||||
isStarted.value = false
|
||||
client.Stop()
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = undefined
|
||||
}
|
||||
signalRClient?.stop()
|
||||
signalRClient = null
|
||||
}
|
||||
async function connectDanmakuClient() {
|
||||
console.log('[WEB-FETCHER] 正在连接弹幕客户端...')
|
||||
const result = await client.Start()
|
||||
if (result.success) {
|
||||
console.log('[WEB-FETCHER] 加载完成, 开始监听弹幕')
|
||||
client.onEvent('all', onGetDanmakus)
|
||||
timer ??= setInterval(() => {
|
||||
sendEvents()
|
||||
}, 1500)
|
||||
} else {
|
||||
console.log('[WEB-FETCHER] 弹幕客户端启动失败: ' + result.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
async function connectSignalR() {
|
||||
console.log('[WEB-FETCHER] 正在连接到 vtsuru 服务器...')
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + route.query.token, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cookie.value}`,
|
||||
},
|
||||
skipNegotiation: true,
|
||||
transport: signalR.HttpTransportType.WebSockets,
|
||||
})
|
||||
.withAutomaticReconnect([0, 2000, 10000, 30000])
|
||||
.withHubProtocol(new msgpack.MessagePackHubProtocol())
|
||||
.build()
|
||||
connection.on('Disconnect', (reason: unknown) => {
|
||||
console.log('[WEB-FETCHER] 被服务器断开连接: ' + reason)
|
||||
disconnectedByServer = true
|
||||
connection.stop()
|
||||
signalRClient = null
|
||||
})
|
||||
connection.on('ConnectClient', async () => {
|
||||
if (client.isRunning) {
|
||||
return
|
||||
}
|
||||
let result = await connectDanmakuClient()
|
||||
while (!result.success) {
|
||||
console.log('[WEB-FETCHER] 弹幕客户端启动失败, 5秒后重试')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
result = await connectDanmakuClient()
|
||||
}
|
||||
isStarted.value = true
|
||||
})
|
||||
|
||||
connection.onclose(reconnect)
|
||||
try {
|
||||
await connection.start()
|
||||
console.log('[WEB-FETCHER] 已连接到 vtsuru 服务器')
|
||||
signalRClient = connection
|
||||
return true
|
||||
} catch (e) {
|
||||
console.log('[WEB-FETCHER] 无法连接到 vtsuru 服务器: ' + e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
async function reconnect() {
|
||||
if (disconnectedByServer) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await signalRClient?.start()
|
||||
console.log('[WEB-FETCHER] 已重新连接')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
setTimeout(reconnect, 5000) // 如果连接失败,则每5秒尝试一次重新启动连接
|
||||
}
|
||||
}
|
||||
function onGetDanmakus(command: any) {
|
||||
events.push(command)
|
||||
}
|
||||
async function sendEvents() {
|
||||
if (signalRClient?.state !== 'Connected') {
|
||||
return
|
||||
}
|
||||
let tempEvents: string[] = []
|
||||
let count = events.length
|
||||
if (events.length > 20) {
|
||||
tempEvents = events.slice(0, 20)
|
||||
count = 20
|
||||
} else {
|
||||
tempEvents = events
|
||||
count = events.length
|
||||
}
|
||||
if (tempEvents.length > 0) {
|
||||
const result = await signalRClient?.invoke<{
|
||||
Success: boolean
|
||||
Message: string
|
||||
}>('UploadEvents', tempEvents, false)
|
||||
if (result?.Success) {
|
||||
events.splice(0, count)
|
||||
console.log(`[WEB-FETCHER] <${format(new Date(), 'HH:mm:ss')}> 上传了 ${count} 条弹幕`)
|
||||
} else {
|
||||
console.error('[WEB-FETCHER] 上传弹幕失败: ' + result?.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Start,
|
||||
Stop,
|
||||
client,
|
||||
isStarted,
|
||||
}
|
||||
})
|
||||
@@ -454,7 +454,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout v-if="accountInfo" style="height: 100vh">
|
||||
<NLayout v-if="accountInfo.id" style="height: 100vh">
|
||||
<NLayoutHeader bordered style="height: 50px; padding: 10px 15px 5px 15px">
|
||||
<NPageHeader>
|
||||
<template #title>
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
|
||||
let client = new DanmakuClient(null)
|
||||
const client = new DanmakuClient(null)
|
||||
const isClientLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -54,12 +54,15 @@ onMounted(() => {
|
||||
}, 1000)
|
||||
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
|
||||
visiable.value = visibility
|
||||
}
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onActiveChange = function (a: boolean) {
|
||||
active.value = a
|
||||
if (window.obsstudio) {
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onVisibilityChange = function (visibility: boolean) {
|
||||
visiable.value = visibility
|
||||
}
|
||||
//@ts-expect-error 这里获取不了
|
||||
window.obsstudio.onActiveChange = function (a: boolean) {
|
||||
active.value = a
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
|
||||
41
src/views/obs/WebFetcherOBS.vue
Normal file
41
src/views/obs/WebFetcherOBS.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { useWebFetcher } from '@/store/useWebFetcher'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const webFetcher = useWebFetcher()
|
||||
|
||||
onMounted(() => {
|
||||
webFetcher.Start()
|
||||
})
|
||||
onUnmounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="web-fetcher-status"
|
||||
:style="{
|
||||
backgroundColor: webFetcher.isStarted ? '#6dc56d' : '#e34a4a',
|
||||
}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.web-fetcher-status {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.2);
|
||||
border: 3px solid #00000033;
|
||||
animation: animated-border 3s infinite;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
src/views/others
Normal file
0
src/views/others
Normal file
Reference in New Issue
Block a user