Merge branch 'dev'

pull/36/head
John Smith 4 years ago
commit 138bf8f7a5

@ -14,6 +14,7 @@
* 支持屏蔽弹幕、合并相似弹幕等设置
* 自带样式生成器
* 支持自动翻译弹幕、醒目留言到日语
* 支持标注打赏用户名的读音(拼音和日文假名)
## 使用方法
### 一、本地使用
@ -28,11 +29,11 @@
**注意事项:**
* 本地使用时不要关闭blivechat.exe那个黑框否则不能继续获取弹幕
* 本地使用时不要关闭blivechat.exe那个黑框否则不能继续获取头像或弹幕
* 样式生成器没有列出所有本地字体,但是可以手动输入本地字体
### 二、公共服务器
请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能发生直播事故
请优先在本地使用,使用公共服务器会有更大的延迟,而且服务器故障时可能发生直播事故
* [公共服务器](http://chat.bilisc.com/)
* [仅样式生成器](https://style.vtbs.moe/)
@ -73,8 +74,14 @@
```
2. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
### nginx配置可选
自建服务器时使用,`sudo vim /etc/nginx/sites-enabled/blivechat.conf`
## 自建服务器相关补充
### 服务器配置
服务器配置在`data/config.ini`,可以配置数据库和允许自动翻译等,编辑后要重启生效
**自建服务器时强烈建议不使用加载器**否则可能因为混合HTTP和HTTPS等原因加载不出来
### 参考nginx配置
`sudo vim /etc/nginx/sites-enabled/blivechat.conf`
```conf
upstream blivechat {

@ -406,7 +406,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
self._close_on_timeout_future = None
else:
logger.warning('Unknown cmd, client: %s, cmd: %d, body: %s', self.request.remote_ip, cmd, body)
except:
except Exception:
logger.exception('on_message error, client: %s, message: %s', self.request.remote_ip, message)
def on_close(self):

@ -44,7 +44,7 @@ class AppConfig:
def load(self, path):
try:
config = configparser.ConfigParser()
config.read(path)
config.read(path, 'utf-8')
app_section = config['app']
self.database_url = app_section['database_url']

@ -1,25 +1,22 @@
[app]
# 数据库配置见https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
database_url = sqlite:///data/database.db
# 允许自动翻译到日语
# Enable auto translate to Japanese
enable_translate = true
# 允许翻译的房间ID以逗号分隔。如果为空允许所有房间
# Comma separated room IDs in which translation are not allowed. If empty, all are allowed
# Example: allow_translate_rooms = 4895312,22347054,21693691
allow_translate_rooms =
# 如果使用了nginx之类的反向代理服务器设置为true
# Set to true if you are using a reverse proxy server such as nginx
tornado_xheaders = false
# 加载器URL本地使用时加载器可以让你先运行OBS再运行blivechat。如果为空不使用加载器
# **自建服务器时强烈建议不使用加载器**否则可能因为混合HTTP和HTTPS等原因加载不出来
# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
# DON'T modify this section
[DEFAULT]
database_url = sqlite:///data/database.db
enable_translate = true
allow_translate_rooms =
tornado_xheaders = false
loader_url =

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 110 KiB

@ -1,5 +1,5 @@
import axios from 'axios'
import {inflate} from 'pako'
import * as pako from 'pako'
import {getUuid4Hex} from '@/utils'
import * as avatar from './avatar'
@ -172,7 +172,7 @@ export default class ChatClientDirect {
case OP_SEND_MSG_REPLY: {
let body = new Uint8Array(data.buffer, offset + HEADER_SIZE, packLen - HEADER_SIZE)
if (ver == WS_BODY_PROTOCOL_VERSION_DEFLATE) {
body = inflate(body)
body = pako.inflate(body)
this.handlerMessage(body)
} else {
try {

@ -18,7 +18,8 @@ export const DEFAULT_CONFIG = {
blockMedalLevel: 0,
relayMessagesByServer: false,
autoTranslate: false
autoTranslate: false,
giftUsernamePronunciation: ''
}
export function setLocalConfig (config) {

@ -5,7 +5,7 @@
</template>
<script>
import {DEFAULT_AVATAR_URL} from '@/api/chat/avatar'
import * as avatar from '@/api/chat/avatar'
export default {
name: 'ImgShadow',
@ -26,8 +26,8 @@ export default {
},
methods: {
onLoadError() {
if (this.showImgUrl !== DEFAULT_AVATAR_URL) {
this.showImgUrl = DEFAULT_AVATAR_URL
if (this.showImgUrl !== avatar.DEFAULT_AVATAR_URL) {
this.showImgUrl = avatar.DEFAULT_AVATAR_URL
}
}
}

@ -24,12 +24,12 @@
<template v-if="pinnedMessage">
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-ticker-renderer"
:avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName" :privilegeType="pinnedMessage.privilegeType"
:avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)" :privilegeType="pinnedMessage.privilegeType"
:title="pinnedMessage.title" :time="pinnedMessage.time"
></membership-item>
<paid-message :key="pinnedMessage.id" v-else
class="style-scope yt-live-chat-ticker-renderer"
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName"
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)"
:time="pinnedMessage.time" :content="pinnedMessageShowContent"
></paid-message>
</template>
@ -98,6 +98,7 @@ export default {
window.clearInterval(this.updateTimerId)
},
methods: {
getShowAuthorName: constants.getShowAuthorName,
needToShow(message) {
let pinTime = this.getPinTime(message)
return (new Date() - message.addTime) / (60 * 1000) < pinTime

@ -122,16 +122,23 @@ export function getPriceConfig (price) {
return PRICE_CONFIGS[PRICE_CONFIGS.length - 1]
}
export function getShowContent(message) {
export function getShowContent (message) {
if (message.translation) {
return `${message.content}${message.translation}`
}
return message.content
}
export function getGiftShowContent(message, showGiftName) {
export function getGiftShowContent (message, showGiftName) {
if (!showGiftName) {
return ''
}
return `Sent ${message.giftName}x${message.num}`
}
export function getShowAuthorName (message) {
if (message.authorNamePronunciation) {
return `${message.authorName}(${message.authorNamePronunciation})`
}
return message.authorName
}

@ -18,17 +18,17 @@
></text-message>
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_GIFT"
class="style-scope yt-live-chat-item-list-renderer"
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)"
:time="message.time" :content="getGiftShowContent(message)"
></paid-message>
<membership-item :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-item-list-renderer"
:avatarUrl="message.avatarUrl" :authorName="message.authorName" :privilegeType="message.privilegeType"
:avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)" :privilegeType="message.privilegeType"
:title="message.title" :time="message.time"
></membership-item>
<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"
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)"
:time="message.time" :content="getShowContent(message)"
></paid-message>
</template>
@ -132,6 +132,7 @@ export default {
return constants.getGiftShowContent(message, this.showGiftName)
},
getShowContent: constants.getShowContent,
getShowAuthorName: constants.getShowAuthorName,
addMessage(message) {
this.addMessages([message])

@ -33,6 +33,10 @@ export default {
advanced: 'Advanced',
relayMessagesByServer: 'Relay messages by server',
autoTranslate: 'Auto translate messages to Japanese (requires relay messages by server)',
giftUsernamePronunciation: 'Pronunciation of gift username',
dontShow: 'None',
pinyin: 'Pinyin',
kana: 'Kana',
roomUrl: 'Room URL',
copy: 'Copy',

@ -33,6 +33,10 @@ export default {
advanced: 'アドバンスド',
relayMessagesByServer: 'サーバを介してメッセージを転送する',
autoTranslate: 'コメントを日本語に翻訳する(サーバを介してメッセージを転送する必要)',
giftUsernamePronunciation: 'スーパーチャットのユーザー名の発音',
dontShow: '非表示',
pinyin: 'ピンイン',
kana: '仮名',
roomUrl: 'ルームのURL',
copy: 'コピー',

@ -33,6 +33,10 @@ export default {
advanced: '高级',
relayMessagesByServer: '通过服务器转发消息',
autoTranslate: '自动翻译弹幕到日语(需要通过服务器转发消息)',
giftUsernamePronunciation: '标注打赏用户名读音',
dontShow: '不显示',
pinyin: '拼音',
kana: '日文假名',
roomUrl: '房间URL',
copy: '复制',

@ -9,7 +9,7 @@
</router-link>
</div>
<div class="version">
v1.5.0
v1.5.1
</div>
<sidebar></sidebar>
</el-aside>
@ -36,7 +36,7 @@ export default {
hideSidebar: true
}
},
created() {
mounted() {
window.addEventListener('resize', this.onResize)
this.onResize()
},

@ -3,7 +3,8 @@ import VueRouter from 'vue-router'
import VueI18n from 'vue-i18n'
import {
Aside, Autocomplete, Badge, Button, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
Input, Main, Menu, MenuItem, Message, Row, Scrollbar, Slider, Submenu, Switch, TabPane, Tabs, Tooltip
Input, Main, Menu, MenuItem, Message, Radio, RadioGroup, Row, Scrollbar, Slider, Submenu, Switch,
TabPane, Tabs, Tooltip
} from 'element-ui'
import axios from 'axios'
@ -42,6 +43,8 @@ Vue.use(Input)
Vue.use(Main)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Radio)
Vue.use(RadioGroup)
Vue.use(Row)
Vue.use(Scrollbar)
Vue.use(Slider)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,54 @@
export const DICT_PINYIN = 'pinyin'
export const DICT_KANA = 'kana'
export class PronunciationConverter {
constructor () {
this.pronunciationMap = new Map()
}
async loadDict (dictName) {
let promise
switch (dictName) {
case DICT_PINYIN:
promise = import('./dictPinyin')
break
case DICT_KANA:
promise = import('./dictKana')
break
default:
return
}
let dictTxt = (await promise).default
let pronunciationMap = new Map()
for (let item of dictTxt.split('\n')) {
if (item.length === 0) {
continue
}
pronunciationMap.set(item.substring(0, 1), item.substring(1))
}
this.pronunciationMap = pronunciationMap
}
getPronunciation (text) {
let res = []
let lastHasPronunciation = null
for (let char of text) {
let pronunciation = this.pronunciationMap.get(char)
if (pronunciation === undefined) {
if (lastHasPronunciation !== null && lastHasPronunciation) {
res.push(' ')
}
lastHasPronunciation = false
res.push(char)
} else {
if (lastHasPronunciation !== null) {
res.push(' ')
}
lastHasPronunciation = true
res.push(pronunciation)
}
}
return res.join('')
}
}

@ -64,6 +64,13 @@
<el-form-item :label="$t('home.autoTranslate')">
<el-switch v-model="form.autoTranslate" :disabled="!serverConfig.enableTranslate || !form.relayMessagesByServer"></el-switch>
</el-form-item>
<el-form-item :label="$t('home.giftUsernamePronunciation')">
<el-radio-group v-model="form.giftUsernamePronunciation">
<el-radio label="">{{$t('home.dontShow')}}</el-radio>
<el-radio label="pinyin">{{$t('home.pinyin')}}</el-radio>
<el-radio label="kana">{{$t('home.kana')}}</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
</el-tabs>

@ -4,6 +4,7 @@
<script>
import {mergeConfig, toBool, toInt} from '@/utils'
import * as pronunciation from '@/utils/pronunciation'
import * as chatConfig from '@/api/chatConfig'
import ChatClientDirect from '@/api/chat/ChatClientDirect'
import ChatClientRelay from '@/api/chat/ChatClientRelay'
@ -18,7 +19,8 @@ export default {
data() {
return {
config: {...chatConfig.DEFAULT_CONFIG},
chatClient: null
chatClient: null,
pronunciationConverter: null
}
},
computed: {
@ -29,9 +31,14 @@ export default {
return this.config.blockUsers.split('\n').filter(val => val)
}
},
created() {
mounted() {
this.initConfig()
this.initChatClient()
if (this.config.giftUsernamePronunciation !== '') {
this.pronunciationConverter = new pronunciation.PronunciationConverter()
this.pronunciationConverter.loadDict(this.config.giftUsernamePronunciation)
}
//
this.$message({
message: 'Loaded',
@ -122,6 +129,7 @@ export default {
avatarUrl: data.avatarUrl,
time: new Date(data.timestamp * 1000),
authorName: data.authorName,
authorNamePronunciation: this.getPronunciation(data.authorName),
price: price,
giftName: data.giftName,
num: data.num
@ -138,6 +146,7 @@ export default {
avatarUrl: data.avatarUrl,
time: new Date(data.timestamp * 1000),
authorName: data.authorName,
authorNamePronunciation: this.getPronunciation(data.authorName),
privilegeType: data.privilegeType,
title: 'New member'
}
@ -155,9 +164,11 @@ export default {
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
authorNamePronunciation: this.getPronunciation(data.authorName),
price: data.price,
time: new Date(data.timestamp * 1000),
content: data.content.trim()
content: data.content.trim(),
translation: data.translation
}
this.$refs.renderer.addMessage(message)
},
@ -214,6 +225,12 @@ export default {
return false
}
return this.$refs.renderer.mergeSimilarGift(authorName, price, giftName, num)
},
getPronunciation(text) {
if (this.pronunciationConverter === null) {
return ''
}
return this.pronunciationConverter.getPronunciation(text)
}
}
}

@ -125,7 +125,7 @@ async def _get_avatar_url_from_web_consumer():
# 限制频率防止被B站ban
await asyncio.sleep(0.2)
except:
except Exception:
logger.exception('_get_avatar_url_from_web_consumer error:')

@ -98,7 +98,7 @@ def _on_translate_done(key, future):
# 缓存
try:
res = future.result()
except:
except Exception:
return
if res is None:
return
@ -140,20 +140,31 @@ class TencentTranslate(TranslateProvider):
logger.warning('TencentTranslate init request failed: status=%d %s', r.status, r.reason)
return False
html = await r.text()
m = re.search(r"""\breauthuri\s*=\s*['"](.+?)['"]""", html)
if m is None:
logger.exception('TencentTranslate init failed: reauthuri not found')
return False
reauthuri = m[1]
async with _http_session.post('https://fanyi.qq.com/api/' + reauthuri) as r:
if r.status != 200:
logger.warning('TencentTranslate init request failed: reauthuri=%s, status=%d %s',
reauthuri, r.status, r.reason)
return False
data = await r.json()
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
logger.exception('TencentTranslate init error:')
return False
m = re.search(r"""\bqtv\s*=\s*['"](.+?)['"]""", html)
if m is None:
logger.exception('TencentTranslate init failed: qtv not found')
qtv = data.get('qtv', None)
if qtv is None:
logger.warning('TencentTranslate init failed: qtv not found')
return False
qtv = m[1]
m = re.search(r"""\bqtk\s*=\s*['"](.+?)['"]""", html)
if m is None:
logger.exception('TencentTranslate init failed: qtk not found')
qtk = data.get('qtk', None)
if qtk is None:
logger.warning('TencentTranslate init failed: qtk not found')
return False
qtk = m[1]
self._qtv = qtv
self._qtk = qtk
@ -162,13 +173,13 @@ class TencentTranslate(TranslateProvider):
async def _reinit_coroutine(self):
try:
while True:
await asyncio.sleep(55 * 60)
await asyncio.sleep(30)
while True:
logger.info('TencentTranslate reinit')
logger.debug('TencentTranslate reinit')
try:
if await self._do_init():
break
except:
except Exception:
logger.exception('TencentTranslate init error:')
await asyncio.sleep(3 * 60)
except asyncio.CancelledError:
@ -232,7 +243,7 @@ class TencentTranslate(TranslateProvider):
self._cool_down_future = asyncio.ensure_future(self._cool_down())
async def _cool_down(self):
logger.warning('TencentTranslate is cooling down')
logger.info('TencentTranslate is cooling down')
self._qtv = self._qtk = ''
try:
while True:
@ -242,10 +253,10 @@ class TencentTranslate(TranslateProvider):
if await self._do_init():
self._fail_count = 0
break
except:
except Exception:
logger.exception('TencentTranslate init error:')
finally:
logger.warning('TencentTranslate finished cooling down')
logger.info('TencentTranslate finished cooling down')
self._cool_down_future = None
@ -341,20 +352,20 @@ class YoudaoTranslate(TranslateProvider):
}
async def _cool_down(self):
logger.warning('YoudaoTranslate is cooling down')
logger.info('YoudaoTranslate is cooling down')
self._has_init = False
try:
while True:
await asyncio.sleep(3 * 60)
try:
is_success = await self.init()
except:
except Exception:
logger.exception('YoudaoTranslate init error:')
continue
if is_success:
break
finally:
logger.warning('YoudaoTranslate finished cooling down')
logger.info('YoudaoTranslate finished cooling down')
self._cool_down_future = None
@ -385,7 +396,7 @@ class BilibiliTranslate(TranslateProvider):
asyncio.ensure_future(self._translate_coroutine(text, future))
# 频率限制一分钟20次
await asyncio.sleep(3.1)
except:
except Exception:
logger.exception('BilibiliTranslate error:')
async def _translate_coroutine(self, text, future):
@ -402,8 +413,10 @@ class BilibiliTranslate(TranslateProvider):
async with _http_session.get(
'https://api.live.bilibili.com/av/v1/SuperChat/messageTranslate',
params={
'parent_area_id': '1',
'area_id': '199',
'room_id': '21396545',
'ruid': '407106379',
'parent_area_id': '9',
'area_id': '371',
'msg': text
}
) as r:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 173 KiB

@ -4,7 +4,7 @@ import asyncio
import aiohttp
VERSION = 'v1.5.0'
VERSION = 'v1.5.1'
def check_update():
@ -24,3 +24,5 @@ async def _do_check_update():
print('---------------------------------------------')
except aiohttp.ClientConnectionError:
print('Failed to check update: connection failed')
except asyncio.TimeoutError:
print('Failed to check update: timeout')

Loading…
Cancel
Save