mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-30 03:07:59 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93f6b2383 | ||
|
|
4c2e216191 | ||
|
|
2a35541dc5 | ||
|
|
a927282e77 | ||
|
|
a7faac5425 | ||
|
|
a22496c355 | ||
|
|
20ec58b965 | ||
|
|
7453d7e890 | ||
|
|
1991e33b9d | ||
|
|
f429a6a03d | ||
|
|
4b98c56965 | ||
|
|
eb43a37ae5 | ||
|
|
1cf6d88460 | ||
|
|
9ddbebbd7e | ||
|
|
ca7f27d536 | ||
|
|
98184e6732 | ||
|
|
3b2616c278 |
@@ -11,7 +11,7 @@
|
||||
* 兼容YouTube直播评论栏的样式
|
||||
* 金瓜子礼物模仿醒目留言显示
|
||||
* 高亮舰队、房管、主播的用户名
|
||||
* 支持屏蔽弹幕、限制最大速度等设置
|
||||
* 支持屏蔽弹幕、合并相似弹幕等设置
|
||||
* 自带样式生成器
|
||||
|
||||
## 使用方法
|
||||
|
||||
2
blivedm
2
blivedm
Submodule blivedm updated: efc7f3b3f8...15669a2084
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -9723,6 +9723,7 @@
|
||||
"fsevents": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-1.2.9.tgz",
|
||||
"integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -18954,6 +18955,11 @@
|
||||
"integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=",
|
||||
"dev": true
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.1.tgz",
|
||||
"integrity": "sha512-MPLPRpD4FNqWq9tTIjYG5LesFouDhdyH0EPY3gVK4DRD5+g4aDqdNSzLIwceulo3Yj+PL1bPh6laE5+H6LTcrQ=="
|
||||
},
|
||||
"serve-index": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npm.taobao.org/serve-index/download/serve-index-1.9.1.tgz",
|
||||
|
||||
BIN
frontend/public/static/img/tutorial/tutorial-1.png
Normal file
BIN
frontend/public/static/img/tutorial/tutorial-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-2.png
Normal file
BIN
frontend/public/static/img/tutorial/tutorial-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-3.png
Normal file
BIN
frontend/public/static/img/tutorial/tutorial-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-4.png
Normal file
BIN
frontend/public/static/img/tutorial/tutorial-4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-5.png
Normal file
BIN
frontend/public/static/img/tutorial/tutorial-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -7,7 +7,6 @@ export const DEFAULT_CONFIG = {
|
||||
mergeSimilarDanmaku: true,
|
||||
showDanmaku: true,
|
||||
showGift: true,
|
||||
maxSpeed: 4,
|
||||
maxNumber: 60,
|
||||
|
||||
blockGiftDanmaku: true,
|
||||
|
||||
@@ -20,6 +20,7 @@ export const GUARD_LEVEL_TO_TEXT = [
|
||||
export const MESSAGE_TYPE_TEXT = 0
|
||||
export const MESSAGE_TYPE_MEMBER = 1
|
||||
export const MESSAGE_TYPE_SUPER_CHAT = 2
|
||||
export const MESSAGE_TYPE_DEL = 3
|
||||
|
||||
// 美元 -> 人民币 汇率
|
||||
const EXCHANGE_RATE = 7
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<ticker class="style-scope yt-live-chat-renderer" :messages="paidMessages" :hidden="paidMessages.length === 0"></ticker>
|
||||
<yt-live-chat-item-list-renderer class="style-scope yt-live-chat-renderer" allow-scroll>
|
||||
<div id="item-scroller" ref="scroller" class="style-scope yt-live-chat-item-list-renderer animated" @scroll="onScroll">
|
||||
<div id="item-offset" class="style-scope yt-live-chat-item-list-renderer" style="height: 1800px;">
|
||||
<div id="items" class="style-scope yt-live-chat-item-list-renderer" style="overflow: hidden; transform: translateY(0px);">
|
||||
<div ref="itemOffset" id="item-offset" class="style-scope yt-live-chat-item-list-renderer" style="height: 0px;">
|
||||
<div ref="items" id="items" class="style-scope yt-live-chat-item-list-renderer" style="overflow: hidden"
|
||||
:style="{transform: `translateY(${Math.floor(scrollPixelsRemaining)}px)`}"
|
||||
>
|
||||
<template v-for="message in messages">
|
||||
<text-message :key="message.id" v-if="message.type === MESSAGE_TYPE_TEXT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
@@ -17,7 +19,7 @@
|
||||
:avatarUrl="message.avatarUrl" :title="message.title" :content="message.content"
|
||||
:time="message.time"
|
||||
></legacy-paid-message>
|
||||
<paid-message :key="message.id" v-else
|
||||
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
|
||||
:time="message.time" :content="message.content"
|
||||
@@ -31,13 +33,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/api/config'
|
||||
import Ticker from './Ticker.vue'
|
||||
import TextMessage from './TextMessage.vue'
|
||||
import LegacyPaidMessage from './LegacyPaidMessage.vue'
|
||||
import PaidMessage from './PaidMessage.vue'
|
||||
import * as constants from './constants'
|
||||
|
||||
// const CHAT_SMOOTH_ANIMATION_TIME_MS = 84
|
||||
const CHAT_SMOOTH_ANIMATION_TIME_MS = 84
|
||||
const SCROLLED_TO_BOTTOM_EPSILON = 15
|
||||
|
||||
export default {
|
||||
@@ -49,9 +52,11 @@ export default {
|
||||
PaidMessage
|
||||
},
|
||||
props: {
|
||||
paidMessages: Array,
|
||||
messages: Array,
|
||||
css: String
|
||||
css: String,
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: config.DEFAULT_CONFIG.maxNumber
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let styleElement = document.createElement('style')
|
||||
@@ -59,9 +64,34 @@ export default {
|
||||
return {
|
||||
MESSAGE_TYPE_TEXT: constants.MESSAGE_TYPE_TEXT,
|
||||
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
|
||||
MESSAGE_TYPE_SUPER_CHAT: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
|
||||
styleElement,
|
||||
canAutoScroll: true
|
||||
messages: [], // 显示的消息
|
||||
paidMessages: [], // 固定在上方的消息
|
||||
|
||||
smoothedMessageQueue: [], // 平滑消息队列,由外部调用addMessages等方法添加
|
||||
emitSmoothedMessageTimerId: null, // 消费平滑消息队列的定时器ID
|
||||
enqueueIntervals: [], // 最近进队列的时间间隔,用来估计下次进队列的时间
|
||||
lastEnqueueTime: null, // 上次进队列的时间
|
||||
estimatedEnqueueInterval: null, // 估计的下次进队列时间间隔
|
||||
|
||||
messagesBuffer: [], // 暂时未显示的消息,当不能自动滚动时会积压在这
|
||||
preinsertHeight: 0, // 插入新消息之前items的高度
|
||||
isSmoothed: true, // 是否平滑滚动,当消息太快时不平滑滚动
|
||||
chatRateMs: 1000, // 用来计算消息速度
|
||||
scrollPixelsRemaining: 0, // 平滑滚动剩余像素
|
||||
scrollTimeRemainingMs: 0, // 平滑滚动剩余时间
|
||||
lastSmoothChatMessageAddMs: null, // 上次showNewMessages时间
|
||||
smoothScrollRafHandle: null, // 平滑滚动requestAnimationFrame句柄
|
||||
lastSmoothScrollUpdate: null, // 平滑滚动上一帧时间
|
||||
|
||||
atBottom: true // 滚动到底部,用来判断能否自动滚动
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canScrollToBottom() {
|
||||
return this.atBottom/* || this.allowScroll*/
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -70,11 +100,11 @@ export default {
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.head.removeChild(this.styleElement)
|
||||
},
|
||||
updated() {
|
||||
if (this.canAutoScroll) {
|
||||
this.scrollToBottom()
|
||||
if (this.emitSmoothedMessageTimerId) {
|
||||
window.clearTimeout(this.emitSmoothedMessageTimerId)
|
||||
this.emitSmoothedMessageTimerId = null
|
||||
}
|
||||
this.clearMessages()
|
||||
},
|
||||
watch: {
|
||||
css(val) {
|
||||
@@ -82,12 +112,340 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addMessage(message) {
|
||||
this.addMessages([message])
|
||||
},
|
||||
addMessages(messages) {
|
||||
this.enqueueMessages(messages)
|
||||
},
|
||||
mergeSimilarText(content) {
|
||||
let res = false
|
||||
this.forEachRecentMessage(5, message => {
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
return true
|
||||
}
|
||||
let longer, shorter
|
||||
if (message.content.length > content.length) {
|
||||
longer = message.content
|
||||
shorter = content
|
||||
} else {
|
||||
longer = content
|
||||
shorter = message.content
|
||||
}
|
||||
if (longer.indexOf(shorter) !== -1 // 长的包含短的
|
||||
&& longer.length - shorter.length < shorter.length // 长度差较小
|
||||
) {
|
||||
message.repeated++
|
||||
res = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return res
|
||||
},
|
||||
mergeSimilarGift(authorName, price) {
|
||||
let res = false
|
||||
this.forEachRecentMessage(5, message => {
|
||||
if (message.type === constants.MESSAGE_TYPE_SUPER_CHAT
|
||||
&& message.content === ''
|
||||
&& message.authorName === authorName
|
||||
) {
|
||||
message.price += price
|
||||
res = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return res
|
||||
},
|
||||
forEachRecentMessage(num, callback) {
|
||||
// 从新到老遍历num条消息
|
||||
for (let i = this.smoothedMessageQueue.length - 1; i >= 0 && num > 0; i--) {
|
||||
let messageGroup = this.smoothedMessageQueue[i]
|
||||
for (let j = messageGroup.length - 1; j >= 0 && num-- > 0; j--) {
|
||||
if (!callback(messageGroup[j])) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let arr of [this.messagesBuffer, this.messages]) {
|
||||
for (let i = arr.length - 1; i >= 0 && num-- > 0; i--) {
|
||||
if (!callback(arr[i])) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
delMessage(id) {
|
||||
this.delMessages([id])
|
||||
},
|
||||
delMessages(ids) {
|
||||
this.enqueueMessages(ids.map(id => {
|
||||
return {
|
||||
id: id,
|
||||
type: constants.MESSAGE_TYPE_DEL
|
||||
}
|
||||
}))
|
||||
},
|
||||
clearMessages() {
|
||||
this.messages = []
|
||||
this.paidMessages = []
|
||||
this.smoothedMessageQueue = []
|
||||
this.messagesBuffer = []
|
||||
this.isSmoothed = true
|
||||
this.lastSmoothChatMessageAddMs = null
|
||||
this.chatRateMs = 1000
|
||||
this.lastSmoothScrollUpdate = null
|
||||
this.scrollTimeRemainingMs = this.scrollPixelsRemaining = 0
|
||||
this.smoothScrollRafHandle = null
|
||||
this.preinsertHeight = 0
|
||||
this.maybeResizeScrollContainer()
|
||||
if (!this.atBottom) {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
},
|
||||
|
||||
enqueueMessages(messages) {
|
||||
if (this.lastEnqueueTime) {
|
||||
let interval = new Date() - this.lastEnqueueTime
|
||||
// 理论上B站发包间隔1S,如果不过滤间隔太短的会导致消息平滑失效
|
||||
if (interval > 100) {
|
||||
this.enqueueIntervals.push(interval)
|
||||
if (this.enqueueIntervals.length > 5) {
|
||||
this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5)
|
||||
}
|
||||
this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals)
|
||||
}
|
||||
}
|
||||
this.lastEnqueueTime = new Date()
|
||||
|
||||
// 只有要显示的消息需要平滑
|
||||
let messageGroup = []
|
||||
for (let message of messages) {
|
||||
messageGroup.push(message)
|
||||
if (message.type !== constants.MESSAGE_TYPE_DEL) {
|
||||
this.smoothedMessageQueue.push(messageGroup)
|
||||
messageGroup = []
|
||||
}
|
||||
}
|
||||
if (messageGroup.length > 0) {
|
||||
this.smoothedMessageQueue.push(messageGroup)
|
||||
}
|
||||
|
||||
if (!this.emitSmoothedMessageTimerId) {
|
||||
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages)
|
||||
}
|
||||
},
|
||||
emitSmoothedMessages() {
|
||||
this.emitSmoothedMessageTimerId = null
|
||||
if (this.smoothedMessageQueue.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 估计的下次进队列剩余时间
|
||||
let estimatedNextEnqueueRemainTime = 10 * 1000
|
||||
if (this.estimatedEnqueueInterval) {
|
||||
estimatedNextEnqueueRemainTime = Math.max(this.lastEnqueueTime - new Date() + this.estimatedEnqueueInterval, 1)
|
||||
}
|
||||
// 最快80ms/条,计算发送的消息数,保证在下次进队列之前消费完队列
|
||||
let groupNumToEmit
|
||||
if (this.smoothedMessageQueue.length < estimatedNextEnqueueRemainTime / 80) {
|
||||
// 队列中消息数很少,每次发1条也能发完
|
||||
groupNumToEmit = 1
|
||||
} else {
|
||||
// 每次发1条以上,保证按最快速度能发完
|
||||
groupNumToEmit = Math.ceil(this.smoothedMessageQueue.length / (estimatedNextEnqueueRemainTime / 80))
|
||||
}
|
||||
|
||||
let messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
|
||||
let mergedGroup = []
|
||||
for (let messageGroup of messageGroups) {
|
||||
for (let message of messageGroup) {
|
||||
mergedGroup.push(message)
|
||||
}
|
||||
}
|
||||
this.handleMessageGroup(mergedGroup)
|
||||
|
||||
if (this.smoothedMessageQueue.length <= 0) {
|
||||
return
|
||||
}
|
||||
let sleepTime
|
||||
if (groupNumToEmit == 1) {
|
||||
// 队列中消息数很少,随便定个80-1000ms的时间
|
||||
sleepTime = estimatedNextEnqueueRemainTime / this.smoothedMessageQueue.length
|
||||
sleepTime *= 0.5 + Math.random()
|
||||
if (sleepTime > 1000) {
|
||||
sleepTime = 1000
|
||||
} else if (sleepTime < 80) {
|
||||
sleepTime = 80
|
||||
}
|
||||
} else {
|
||||
// 按最快速度发
|
||||
sleepTime = 80
|
||||
}
|
||||
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages, sleepTime)
|
||||
},
|
||||
|
||||
handleMessageGroup(messageGroup) {
|
||||
if (messageGroup.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let message of messageGroup) {
|
||||
switch (message.type) {
|
||||
case constants.MESSAGE_TYPE_TEXT:
|
||||
case constants.MESSAGE_TYPE_MEMBER:
|
||||
case constants.MESSAGE_TYPE_SUPER_CHAT:
|
||||
this.handleAddMessage(message)
|
||||
break
|
||||
case constants.MESSAGE_TYPE_DEL:
|
||||
this.handleDelMessage(message.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.maybeResizeScrollContainer(),
|
||||
this.flushMessagesBuffer()
|
||||
this.$nextTick(this.maybeScrollToBottom)
|
||||
},
|
||||
handleAddMessage(message) {
|
||||
message = {
|
||||
...message,
|
||||
addTime: new Date() // 添加一个本地时间给Ticker用,防止本地时间和服务器时间相差很大的情况
|
||||
}
|
||||
this.messagesBuffer.push(message)
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
this.paidMessages.unshift(message)
|
||||
}
|
||||
},
|
||||
handleDelMessage(message) {
|
||||
let id = message.id
|
||||
for (let arr of [this.messages, this.paidMessages, this.messagesBuffer]) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].id === id) {
|
||||
arr.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async flushMessagesBuffer() {
|
||||
if (this.messagesBuffer.length <= 0) {
|
||||
return
|
||||
}
|
||||
if (!this.canScrollToBottom) {
|
||||
if (this.messagesBuffer.length > this.maxNumber) {
|
||||
// 未显示消息数 > 最大可显示数,丢弃
|
||||
this.messagesBuffer.splice(0, this.messagesBuffer.length - this.maxNumber)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let removeNum = Math.max(this.messages.length + this.messagesBuffer.length - this.maxNumber, 0)
|
||||
if (removeNum > 0) {
|
||||
this.messages.splice(0, removeNum)
|
||||
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
|
||||
await this.$nextTick()
|
||||
}
|
||||
|
||||
this.preinsertHeight = this.$refs.items.clientHeight
|
||||
for (let message of this.messagesBuffer) {
|
||||
this.messages.push(message)
|
||||
}
|
||||
this.messagesBuffer = []
|
||||
// 等items高度变化
|
||||
this.$nextTick(this.showNewMessages)
|
||||
},
|
||||
showNewMessages() {
|
||||
let hasScrollBar = this.$refs.items.clientHeight > this.$refs.scroller.clientHeight
|
||||
this.$refs.itemOffset.style.height = `${this.$refs.items.clientHeight}px`
|
||||
if (!this.canScrollToBottom || !hasScrollBar) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算剩余像素
|
||||
this.scrollPixelsRemaining += this.$refs.items.clientHeight - this.preinsertHeight
|
||||
this.scrollToBottom()
|
||||
|
||||
// 计算是否平滑滚动、剩余时间
|
||||
if (!this.lastSmoothChatMessageAddMs) {
|
||||
this.lastSmoothChatMessageAddMs = performance.now()
|
||||
}
|
||||
let interval = performance.now() - this.lastSmoothChatMessageAddMs
|
||||
this.chatRateMs = 0.9 * this.chatRateMs + 0.1 * interval
|
||||
if (this.isSmoothed) {
|
||||
if (this.chatRateMs < 400) {
|
||||
this.isSmoothed = false
|
||||
}
|
||||
} else {
|
||||
if (this.chatRateMs > 450) {
|
||||
this.isSmoothed = true
|
||||
}
|
||||
}
|
||||
this.scrollTimeRemainingMs += this.isSmoothed ? CHAT_SMOOTH_ANIMATION_TIME_MS : 0
|
||||
|
||||
if (!this.smoothScrollRafHandle) {
|
||||
this.smoothScrollRafHandle = window.requestAnimationFrame(this.smoothScroll)
|
||||
}
|
||||
this.lastSmoothChatMessageAddMs = performance.now()
|
||||
},
|
||||
smoothScroll(time) {
|
||||
if (!this.lastSmoothScrollUpdate) {
|
||||
// 第一帧
|
||||
this.lastSmoothScrollUpdate = time
|
||||
this.smoothScrollRafHandle = window.requestAnimationFrame(this.smoothScroll)
|
||||
return
|
||||
}
|
||||
|
||||
let interval = time - this.lastSmoothScrollUpdate
|
||||
if (
|
||||
this.scrollPixelsRemaining <= 0 || this.scrollPixelsRemaining >= 400 // 已经滚动到底部或者离底部太远则结束
|
||||
|| interval >= 1000 // 离上一帧时间太久,可能用户切换到其他网页
|
||||
|| this.scrollTimeRemainingMs <= 0 // 时间已结束
|
||||
) {
|
||||
this.resetSmoothScroll()
|
||||
return
|
||||
}
|
||||
|
||||
let pixelsToScroll = interval / this.scrollTimeRemainingMs * this.scrollPixelsRemaining
|
||||
this.scrollPixelsRemaining -= pixelsToScroll
|
||||
if (this.scrollPixelsRemaining < 0) {
|
||||
this.scrollPixelsRemaining = 0
|
||||
}
|
||||
this.scrollTimeRemainingMs -= interval
|
||||
if (this.scrollTimeRemainingMs < 0) {
|
||||
this.scrollTimeRemainingMs = 0
|
||||
}
|
||||
this.lastSmoothScrollUpdate = time
|
||||
this.smoothScrollRafHandle = window.requestAnimationFrame(this.smoothScroll)
|
||||
},
|
||||
resetSmoothScroll() {
|
||||
this.scrollTimeRemainingMs = this.scrollPixelsRemaining = 0
|
||||
this.lastSmoothScrollUpdate = null
|
||||
if (this.smoothScrollRafHandle) {
|
||||
window.cancelAnimationFrame(this.smoothScrollRafHandle)
|
||||
this.smoothScrollRafHandle = null
|
||||
}
|
||||
},
|
||||
|
||||
maybeResizeScrollContainer() {
|
||||
this.$refs.itemOffset.style.height = `${this.$refs.items.clientHeight}px`
|
||||
this.maybeScrollToBottom()
|
||||
},
|
||||
maybeScrollToBottom() {
|
||||
if (this.canScrollToBottom) {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$refs.scroller.scrollTop = this.$refs.scroller.scrollHeight
|
||||
this.$refs.scroller.scrollTop = Math.pow(2, 24)
|
||||
this.atBottom = true
|
||||
},
|
||||
onScroll() {
|
||||
let scroller = this.$refs.scroller
|
||||
this.canAutoScroll = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON
|
||||
this.atBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON
|
||||
this.flushMessagesBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
sidebar: {
|
||||
home: 'Home',
|
||||
stylegen: 'Style generator',
|
||||
help: 'Help',
|
||||
projectAddress: 'Project address',
|
||||
giftRecord: 'Super Chat record'
|
||||
},
|
||||
@@ -15,7 +16,6 @@ export default {
|
||||
showGift: 'Show Super Chats',
|
||||
mergeSimilarDanmaku: 'Merge similar messages',
|
||||
minGiftPrice: 'Min price of Super Chats to show (CNY)',
|
||||
maxSpeed: 'Max speed of messages (/second, 0 for unlimited)',
|
||||
maxNumber: 'Max number of messages',
|
||||
|
||||
block: 'Block',
|
||||
@@ -107,5 +107,13 @@ export default {
|
||||
result: 'Result',
|
||||
copy: 'Copy',
|
||||
resetConfig: 'Reset config'
|
||||
},
|
||||
help: {
|
||||
help: 'Help',
|
||||
p1: '1. Copy the room ID from the Bilibili live room webpage',
|
||||
p2: '2. Enter the room ID into the room ID on the home page. Copy the room URL after saving the configuration',
|
||||
p3: '3. Generate styles with the style generator. Copy the CSS',
|
||||
p4: '4. Add browser source in OBS',
|
||||
p5: '5. Enter the previously copied room URL at URL, and enter the previously copied CSS at custom CSS'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'トップページ',
|
||||
stylegen: 'スタイルジェネレータ',
|
||||
help: 'ヘルプ',
|
||||
projectAddress: 'プロジェクトアドレス',
|
||||
giftRecord: 'スーパーチャット記録'
|
||||
},
|
||||
@@ -15,7 +16,6 @@
|
||||
showGift: 'スーパーチャットと新メンバーを表示する',
|
||||
mergeSimilarDanmaku: '同じコメントを合併する',
|
||||
minGiftPrice: '最低表示スーパーチャット価格(CNY)',
|
||||
maxSpeed: 'コメントの最大速度(バー/秒、0は無制限)',
|
||||
maxNumber: '最大コメント数',
|
||||
|
||||
block: 'ブロック',
|
||||
@@ -107,5 +107,13 @@
|
||||
result: '結果',
|
||||
copy: 'コピー',
|
||||
resetConfig: 'デフォルトに戻す'
|
||||
},
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
p1: '1. ビリビリの生放送ウェブから生放送ルームIDをこぴーする',
|
||||
p2: '2. ホームページでコピーしたIDを入力し、配置を保存すると、ルームのURLをこぴーする',
|
||||
p3: '3. スタイルジェネレータでお好みのコメント様子を選び、出力したCSSをコピーする',
|
||||
p4: '4. OBSでブラウザを新規作成する',
|
||||
p5: '5. プロパティでこぴーしたURLを入力し、カスタムCSSでスタイルジェネレータのCSSを入力する'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
sidebar: {
|
||||
home: '首页',
|
||||
stylegen: '样式生成器',
|
||||
help: '帮助',
|
||||
projectAddress: '项目地址',
|
||||
giftRecord: '打赏记录'
|
||||
},
|
||||
@@ -15,7 +16,6 @@ export default {
|
||||
showGift: '显示打赏和新舰长',
|
||||
mergeSimilarDanmaku: '合并相似弹幕',
|
||||
minGiftPrice: '最低显示打赏价格(元)',
|
||||
maxSpeed: '弹幕最大速度(条/秒,0为无限制)',
|
||||
maxNumber: '最大弹幕数',
|
||||
|
||||
block: '屏蔽',
|
||||
@@ -107,5 +107,13 @@ export default {
|
||||
result: '结果',
|
||||
copy: '复制',
|
||||
resetConfig: '恢复默认设置'
|
||||
},
|
||||
help: {
|
||||
help: '帮助',
|
||||
p1: '1. 从B站直播间网页复制房间ID',
|
||||
p2: '2. 把房间ID输入到首页的房间ID,保存配置后复制房间URL',
|
||||
p3: '3. 使用样式生成器生成样式,复制CSS',
|
||||
p4: '4. 在OBS中添加浏览器源',
|
||||
p5: '5. URL处输入之前复制的房间URL,自定义CSS处输入之前复制的CSS'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<el-menu-item :index="$router.resolve({name: 'stylegen'}).href">
|
||||
<i class="el-icon-brush"></i>{{$t('sidebar.stylegen')}}
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="$router.resolve({name: 'help'}).href">
|
||||
<i class="el-icon-question"></i>{{$t('sidebar.help')}}
|
||||
</el-menu-item>
|
||||
<a href="https://github.com/xfgryujk/blivechat" target="_blank">
|
||||
<el-menu-item>
|
||||
<i class="el-icon-share"></i>{{$t('sidebar.projectAddress')}}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<h1 class="sidebar-title">blivechat</h1>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="version">
|
||||
v1.2.3
|
||||
</div>
|
||||
<sidebar></sidebar>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
@@ -118,6 +121,17 @@ a, a:focus, a:hover {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-container .version {
|
||||
height: 30px;
|
||||
background: #2b2f3a;
|
||||
color: #aaa;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-container .is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import App from './App.vue'
|
||||
import Layout from './layout'
|
||||
import Home from './views/Home.vue'
|
||||
import StyleGenerator from './views/StyleGenerator'
|
||||
import Help from './views/Help'
|
||||
import Room from './views/Room.vue'
|
||||
import NotFound from './views/NotFound.vue'
|
||||
|
||||
@@ -37,7 +38,8 @@ const router = new VueRouter({
|
||||
component: Layout,
|
||||
children: [
|
||||
{path: '', component: Home},
|
||||
{path: 'stylegen', name: 'stylegen', component: StyleGenerator}
|
||||
{path: 'stylegen', name: 'stylegen', component: StyleGenerator},
|
||||
{path: 'help', name: 'help', component: Help}
|
||||
]
|
||||
},
|
||||
{path: '/room/:roomId', name: 'room', component: Room},
|
||||
|
||||
21
frontend/src/views/Help.vue
Normal file
21
frontend/src/views/Help.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{$t('help.help')}}</h1>
|
||||
<p>{{$t('help.p1')}}</p>
|
||||
<p><el-image src="/static/img/tutorial/tutorial-1.png"></el-image></p>
|
||||
<p>{{$t('help.p2')}}</p>
|
||||
<p><el-image src="/static/img/tutorial/tutorial-2.png"></el-image></p>
|
||||
<p>{{$t('help.p3')}}</p>
|
||||
<p><el-image src="/static/img/tutorial/tutorial-3.png"></el-image></p>
|
||||
<p>{{$t('help.p4')}}</p>
|
||||
<p><el-image src="/static/img/tutorial/tutorial-4.png"></el-image></p>
|
||||
<p>{{$t('help.p5')}}</p>
|
||||
<p><el-image src="/static/img/tutorial/tutorial-5.png"></el-image></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Help'
|
||||
}
|
||||
</script>
|
||||
@@ -22,9 +22,6 @@
|
||||
<el-form-item :label="$t('home.minGiftPrice')">
|
||||
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.maxSpeed')">
|
||||
<el-input v-model.number="form.maxSpeed" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.maxNumber')">
|
||||
<el-input v-model.number="form.maxNumber" type="number" min="1"></el-input>
|
||||
</el-form-item>
|
||||
@@ -119,7 +116,7 @@ export default {
|
||||
|
||||
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId},
|
||||
query: {config_id: window.localStorage.configId}})
|
||||
this.roomUrl = `http://${window.location.host}${resolved.href}`
|
||||
this.roomUrl = `${window.location.protocol}//${window.location.host}${resolved.href}`
|
||||
})
|
||||
},
|
||||
enterRoom() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<chat-renderer :paidMessages="paidMessages" :messages="messages" :css="config.css"></chat-renderer>
|
||||
<chat-renderer ref="renderer" :css="config.css" :maxNumber="config.maxNumber"></chat-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -21,23 +21,23 @@ export default {
|
||||
ChatRenderer
|
||||
},
|
||||
data() {
|
||||
let cfg = {...config.DEFAULT_CONFIG}
|
||||
cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
|
||||
cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
|
||||
cfg.maxSpeed = 0
|
||||
return {
|
||||
config: cfg,
|
||||
config: {...config.DEFAULT_CONFIG},
|
||||
|
||||
websocket: null,
|
||||
retryCount: 0,
|
||||
isDestroying: false,
|
||||
heartbeatTimerId: null,
|
||||
|
||||
messagesBufferTimerId: null,
|
||||
nextId: 0,
|
||||
messagesBuffer: [], // 暂时不显示的消息,可能会丢弃
|
||||
messages: [], // 正在显示的消息
|
||||
paidMessages: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
blockKeywords() {
|
||||
return this.config.blockKeywords.split('\n').filter(val => val)
|
||||
},
|
||||
blockUsers() {
|
||||
return this.config.blockUsers.split('\n').filter(val => val)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -47,30 +47,13 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.messagesBufferTimerId) {
|
||||
window.clearInterval(this.messagesBufferTimerId)
|
||||
}
|
||||
this.isDestroying = true
|
||||
this.websocket.close()
|
||||
},
|
||||
watch: {
|
||||
config(val) {
|
||||
if (this.messagesBufferTimerId) {
|
||||
window.clearInterval(this.messagesBufferTimerId)
|
||||
this.messagesBufferTimerId = null
|
||||
}
|
||||
if (val.maxSpeed > 0) {
|
||||
this.messagesBufferTimerId = window.setInterval(this.handleMessagesBuffer, 1000 / val.maxSpeed)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateConfig(configId) {
|
||||
try {
|
||||
let cfg = await config.getRemoteConfig(configId)
|
||||
cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
|
||||
cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
|
||||
this.config = cfg
|
||||
this.config = await config.getRemoteConfig(configId)
|
||||
} catch (e) {
|
||||
this.$message.error('获取配置失败:' + e)
|
||||
}
|
||||
@@ -117,7 +100,7 @@ export default {
|
||||
let time = data.timestamp ? new Date(data.timestamp * 1000) : new Date()
|
||||
switch (cmd) {
|
||||
case COMMAND_ADD_TEXT:
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilar(data.content)) {
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
@@ -140,6 +123,9 @@ export default {
|
||||
if (price < this.config.minGiftPrice) { // 丢人
|
||||
break
|
||||
}
|
||||
if (this.mergeSimilarGift(data.authorName, price)) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
id: `gift_${this.nextId++}`,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
@@ -179,26 +165,18 @@ export default {
|
||||
authorName: data.authorName,
|
||||
price: data.price,
|
||||
time: time,
|
||||
content: data.content
|
||||
content: data.content.trim()
|
||||
}
|
||||
break
|
||||
case COMMAND_DEL_SUPER_CHAT: {
|
||||
let arrays = [this.messages, this.messagesBuffer, this.paidMessages]
|
||||
case COMMAND_DEL_SUPER_CHAT:
|
||||
for (let id of data.ids) {
|
||||
id = `sc_${id}`
|
||||
for (let arr of arrays) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].id === id) {
|
||||
arr.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$refs.renderer.delMessage(id)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (message) {
|
||||
this.addMessageBuffer(message)
|
||||
this.$refs.renderer.addMessage(message)
|
||||
}
|
||||
},
|
||||
filterTextMessage(data) {
|
||||
@@ -213,89 +191,32 @@ export default {
|
||||
} else if (this.config.blockMedalLevel > 0 && data.medalLevel < this.config.blockMedalLevel) {
|
||||
return false
|
||||
}
|
||||
for (let keyword of this.config.blockKeywords) {
|
||||
if (data.content.indexOf(keyword) !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let user of this.config.blockUsers) {
|
||||
if (data.authorName === user) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return this.filterSuperChatMessage(data)
|
||||
},
|
||||
filterSuperChatMessage(data) {
|
||||
for (let keyword of this.config.blockKeywords) {
|
||||
for (let keyword of this.blockKeywords) {
|
||||
if (data.content.indexOf(keyword) !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let user of this.config.blockUsers) {
|
||||
for (let user of this.blockUsers) {
|
||||
if (data.authorName === user) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
mergeSimilar(content) {
|
||||
mergeSimilarText(content) {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
return false
|
||||
}
|
||||
for (let i = this.messages.length - 1; i >= 0 && i >= this.messages.length - 5; i--) {
|
||||
let message = this.messages[i]
|
||||
if (
|
||||
(message.content.indexOf(content) !== -1 || content.indexOf(message.content) !== -1) // 包含对方
|
||||
&& Math.abs(message.content.length - content.length) < Math.min(message.content.length, content.length) // 长度差比两者长度都小
|
||||
) {
|
||||
message.repeated++
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return this.$refs.renderer.mergeSimilarText(content)
|
||||
},
|
||||
addMessageBuffer(message) {
|
||||
if (this.config.maxSpeed > 0 && message.type === constants.MESSAGE_TYPE_TEXT) {
|
||||
message.addTime = new Date()
|
||||
this.messagesBuffer.push(message)
|
||||
} else {
|
||||
// 无速度限制或者是礼物
|
||||
this.addMessageShow(message)
|
||||
mergeSimilarGift(authorName, price) {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
addMessageShow(message) {
|
||||
message.addTime = new Date()
|
||||
this.messages.push(message)
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
this.paidMessages.unshift(message)
|
||||
}
|
||||
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
|
||||
this.$nextTick(() => {
|
||||
if (this.messages.length > this.config.maxNumber) {
|
||||
this.messages.splice(0, this.messages.length - this.config.maxNumber)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleMessagesBuffer() {
|
||||
// 3秒内未发出则丢弃
|
||||
let i
|
||||
let curTime = new Date()
|
||||
for (i = 0; i < this.messagesBuffer.length; i++) {
|
||||
if (curTime - this.messagesBuffer[i].addTime <= 3 * 1000) {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.messagesBuffer.splice(0, i)
|
||||
if (this.messagesBuffer.length <= 0) {
|
||||
return
|
||||
}
|
||||
let message = this.messagesBuffer.shift()
|
||||
// 防止短时间发送多条时不能合并
|
||||
if (this.mergeSimilar(message.content)) {
|
||||
this.handleMessagesBuffer()
|
||||
return
|
||||
}
|
||||
this.addMessageShow(message)
|
||||
return this.$refs.renderer.mergeSimilarGift(authorName, price)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +188,9 @@
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div id="example-container" ref="exampleContainer">
|
||||
<div ref="exampleContainer" id="example-container">
|
||||
<div id="fakebody">
|
||||
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss" ref="example"></chat-renderer>
|
||||
<chat-renderer ref="renderer" :css="exampleCss"></chat-renderer>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -246,7 +246,8 @@ const EXAMPLE_MESSAGES = [
|
||||
authorName: 'mob路人',
|
||||
content: '8888888888',
|
||||
repeated: 12
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'member舰长',
|
||||
@@ -254,30 +255,35 @@ const EXAMPLE_MESSAGES = [
|
||||
content: '草',
|
||||
privilegeType: 3,
|
||||
repeated: 3
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'admin房管',
|
||||
authorType: constants.AUTHRO_TYPE_ADMIN,
|
||||
content: 'kksk'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...legacyPaidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '吾乐KANA',
|
||||
content: 'Welcome 吾乐KANA!'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '无火的残渣',
|
||||
price: 66600,
|
||||
content: 'Sent 小电视飞船x100'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'streamer主播',
|
||||
authorType: constants.AUTHRO_TYPE_OWNER,
|
||||
content: '感谢石油佬送的小电视'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '夏色祭保護協会会長',
|
||||
@@ -285,12 +291,6 @@ const EXAMPLE_MESSAGES = [
|
||||
content: 'Sent 礼花x1'
|
||||
}
|
||||
]
|
||||
let examplePaidMessages = []
|
||||
for (let message of EXAMPLE_MESSAGES) {
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
examplePaidMessages.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'StyleGenerator',
|
||||
@@ -298,15 +298,14 @@ export default {
|
||||
ChatRenderer
|
||||
},
|
||||
data() {
|
||||
let config = stylegen.getLocalConfig()
|
||||
let result = stylegen.getStyle(config)
|
||||
let stylegenConfig = stylegen.getLocalConfig()
|
||||
let result = stylegen.getStyle(stylegenConfig)
|
||||
return {
|
||||
FONTS: [...fonts.LOCAL_FONTS, ...fonts.NETWORK_FONTS],
|
||||
form: {...config},
|
||||
|
||||
form: {...stylegenConfig},
|
||||
result,
|
||||
exampleCss: result.replace(/^body\b/gm, '#fakebody'),
|
||||
messages: EXAMPLE_MESSAGES,
|
||||
examplePaidMessages
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -314,8 +313,19 @@ export default {
|
||||
return stylegen.getStyle(this.form)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
computedResult: _.debounce(function(val) {
|
||||
this.result = val
|
||||
stylegen.setLocalConfig(this.form)
|
||||
}, 500),
|
||||
result(val) {
|
||||
this.exampleCss = val.replace(/^body\b/gm, '#fakebody')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let observer = new MutationObserver(() => this.$refs.example.scrollToBottom())
|
||||
this.$refs.renderer.addMessages(EXAMPLE_MESSAGES)
|
||||
|
||||
let observer = new MutationObserver(() => this.$refs.renderer.scrollToBottom())
|
||||
observer.observe(this.$refs.exampleContainer, {attributes: true})
|
||||
},
|
||||
methods: {
|
||||
@@ -330,8 +340,8 @@ export default {
|
||||
callback(res)
|
||||
},
|
||||
playAnimation() {
|
||||
this.messages = []
|
||||
this.$nextTick(() => this.messages = EXAMPLE_MESSAGES)
|
||||
this.$refs.renderer.clearMessages()
|
||||
this.$nextTick(() => this.$refs.renderer.addMessages(EXAMPLE_MESSAGES))
|
||||
},
|
||||
copyResult() {
|
||||
this.$refs.result.select()
|
||||
@@ -340,15 +350,6 @@ export default {
|
||||
resetConfig() {
|
||||
this.form = {...stylegen.DEFAULT_CONFIG}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
computedResult: _.debounce(function(val) {
|
||||
this.result = val
|
||||
stylegen.setLocalConfig(this.form)
|
||||
}, 500),
|
||||
result(val) {
|
||||
this.exampleCss = val.replace(/^body\b/gm, '#fakebody')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
19
update.py
19
update.py
@@ -2,14 +2,17 @@
|
||||
|
||||
import aiohttp
|
||||
|
||||
VERSION = 'v1.2.2'
|
||||
VERSION = 'v1.2.3'
|
||||
|
||||
|
||||
async def check_update():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('https://api.github.com/repos/xfgryujk/blivechat/releases/latest') as r:
|
||||
data = await r.json()
|
||||
if data['name'] != VERSION:
|
||||
print('New version available:', data['name'])
|
||||
print(data['body'])
|
||||
print('Download:', data['html_url'])
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('https://api.github.com/repos/xfgryujk/blivechat/releases/latest') as r:
|
||||
data = await r.json()
|
||||
if data['name'] != VERSION:
|
||||
print('New version available:', data['name'])
|
||||
print(data['body'])
|
||||
print('Download:', data['html_url'])
|
||||
except aiohttp.ClientConnectionError:
|
||||
print('Failed to check update: connection failed')
|
||||
|
||||
@@ -26,7 +26,7 @@ class Command(enum.IntEnum):
|
||||
DEL_SUPER_CHAT = 6
|
||||
|
||||
|
||||
DEFAULT_AVATAR_URL = 'https://static.hdslb.com/images/member/noface.gif'
|
||||
DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
|
||||
|
||||
_http_session = aiohttp.ClientSession()
|
||||
_avatar_url_cache: Dict[int, str] = {}
|
||||
@@ -68,7 +68,7 @@ async def get_avatar_url(user_id):
|
||||
data = await r.json()
|
||||
except aiohttp.ClientConnectionError:
|
||||
return DEFAULT_AVATAR_URL
|
||||
url = data['data']['face']
|
||||
url = data['data']['face'].replace('http:', '').replace('https:', '')
|
||||
if not url.endswith('noface.gif'):
|
||||
url += '@48w_48h'
|
||||
_avatar_url_cache[user_id] = url
|
||||
@@ -96,7 +96,64 @@ async def _fetch_avatar_loop():
|
||||
asyncio.ensure_future(_fetch_avatar_loop())
|
||||
|
||||
|
||||
# 重新定义parse_XXX是为了减少对字段名的依赖,防止B站改字段名
|
||||
def _parse_danmaku(client: blivedm.BLiveClient, command):
|
||||
info = command['info']
|
||||
if info[3]:
|
||||
room_id = info[3][3]
|
||||
medal_level = info[3][0]
|
||||
else:
|
||||
room_id = medal_level = 0
|
||||
return client._on_receive_danmaku(blivedm.DanmakuMessage(
|
||||
None, None, None, info[0][4], None, None, info[0][9], None,
|
||||
info[1],
|
||||
info[2][0], info[2][1], info[2][2], None, None, info[2][5], info[2][6], None,
|
||||
medal_level, None, None, room_id, None, None,
|
||||
info[4][0], None, None,
|
||||
None, None,
|
||||
info[7]
|
||||
))
|
||||
|
||||
|
||||
def _parse_gift(client: blivedm.BLiveClient, command):
|
||||
data = command['data']
|
||||
return client._on_receive_gift(blivedm.GiftMessage(
|
||||
data['giftName'], data['num'], data['uname'], data['face'], None,
|
||||
None, data['timestamp'], None, None,
|
||||
None, None, None, data['coin_type'], data['total_coin']
|
||||
))
|
||||
|
||||
|
||||
def _parse_buy_guard(client: blivedm.BLiveClient, command):
|
||||
data = command['data']
|
||||
return client._on_buy_guard(blivedm.GuardBuyMessage(
|
||||
data['uid'], data['username'], None, None, None,
|
||||
None, None, data['start_time'], None
|
||||
))
|
||||
|
||||
|
||||
def _parse_super_chat(client: blivedm.BLiveClient, command):
|
||||
data = command['data']
|
||||
return client._on_super_chat(blivedm.SuperChatMessage(
|
||||
data['price'], data['message'], None, data['start_time'],
|
||||
None, None, data['id'], None,
|
||||
None, None, data['user_info']['uname'],
|
||||
data['user_info']['face'], None,
|
||||
None, None,
|
||||
None, None, None,
|
||||
None
|
||||
))
|
||||
|
||||
|
||||
class Room(blivedm.BLiveClient):
|
||||
_COMMAND_HANDLERS = {
|
||||
**blivedm.BLiveClient._COMMAND_HANDLERS,
|
||||
'DANMU_MSG': _parse_danmaku,
|
||||
'SEND_GIFT': _parse_gift,
|
||||
'GUARD_BUY': _parse_buy_guard,
|
||||
'SUPER_CHAT_MESSAGE': _parse_super_chat
|
||||
}
|
||||
|
||||
def __init__(self, room_id):
|
||||
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
|
||||
self.clients: List['ChatHandler'] = []
|
||||
@@ -111,7 +168,10 @@ class Room(blivedm.BLiveClient):
|
||||
def send_message(self, cmd, data):
|
||||
body = json.dumps({'cmd': cmd, 'data': data})
|
||||
for client in self.clients:
|
||||
client.write_message(body)
|
||||
try:
|
||||
client.write_message(body)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
pass
|
||||
|
||||
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
|
||||
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
|
||||
@@ -143,7 +203,7 @@ class Room(blivedm.BLiveClient):
|
||||
if gift.coin_type != 'gold': # 丢人
|
||||
return
|
||||
self.send_message(Command.ADD_GIFT, {
|
||||
'avatarUrl': gift.face,
|
||||
'avatarUrl': gift.face.replace('http:', '').replace('https:', ''),
|
||||
'timestamp': gift.timestamp,
|
||||
'authorName': gift.uname,
|
||||
'giftName': gift.gift_name,
|
||||
@@ -163,7 +223,7 @@ class Room(blivedm.BLiveClient):
|
||||
|
||||
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
|
||||
self.send_message(Command.ADD_SUPER_CHAT, {
|
||||
'avatarUrl': message.face,
|
||||
'avatarUrl': message.face.replace('http:', '').replace('https:', ''),
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.uname,
|
||||
'price': message.price,
|
||||
@@ -266,7 +326,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
# 测试用
|
||||
def send_test_message(self):
|
||||
base_data = {
|
||||
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
|
||||
'avatarUrl': '//i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
|
||||
'timestamp': time.time(),
|
||||
'authorName': 'xfgryujk',
|
||||
}
|
||||
@@ -312,4 +372,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
|
||||
def send_message(self, cmd, data):
|
||||
body = json.dumps({'cmd': cmd, 'data': data})
|
||||
self.write_message(body)
|
||||
try:
|
||||
self.write_message(body)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user