Compare commits

..

8 Commits

Author SHA1 Message Date
John Smith
63644f5499 修复qtv、qtk反了 2020-03-21 19:13:06 +08:00
John Smith
8a57304790 更新版本号v1.4.2 2020-03-21 19:05:53 +08:00
John Smith
d7df071595 修复启动长时间后自动翻译失效的问题 2020-03-21 18:42:27 +08:00
John Smith
6e8b04ef4f 修复删缓存时的错误、提高容错性 2020-03-21 15:23:31 +08:00
John Smith
b73a4e3748 更新版本号v1.4.1 2020-03-01 14:22:12 +08:00
John Smith
102746d0b0 添加显示礼物名、只合并同类礼物 2020-03-01 14:14:19 +08:00
John Smith
2d237af6fd 提示用户已加载页面 2020-02-29 19:20:29 +08:00
John Smith
4030ff43cd 减小编译后文件尺寸 2020-02-22 17:21:47 +08:00
20 changed files with 4776 additions and 11432 deletions

View File

@@ -63,7 +63,7 @@ class Room(blivedm.BLiveClient):
def __parse_gift(self, command):
data = command['data']
return self._on_receive_gift(blivedm.GiftMessage(
None, None, data['uname'], data['face'], None,
data['giftName'], data['num'], data['uname'], data['face'], None,
data['uid'], data['timestamp'], None, None,
None, None, None, data['coin_type'], data['total_coin']
))
@@ -113,7 +113,7 @@ class Room(blivedm.BLiveClient):
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
pass
room_manager.del_client(self.room_id, client)
def send_message_if(self, can_send_func: Callable[['ChatHandler'], bool], cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
@@ -121,7 +121,7 @@ class Room(blivedm.BLiveClient):
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
pass
room_manager.del_client(self.room_id, client)
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
@@ -192,7 +192,9 @@ class Room(blivedm.BLiveClient):
'avatarUrl': avatar_url,
'timestamp': gift.timestamp,
'authorName': gift.uname,
'totalCoin': gift.total_coin
'totalCoin': gift.total_coin,
'giftName': gift.gift_name,
'num': gift.num
})
async def _on_buy_guard(self, message: blivedm.GuardBuyMessage):
@@ -273,7 +275,10 @@ class RoomManager:
if not await self._add_room(room_id):
client.close()
return
room = self._rooms[room_id]
room = self._rooms.get(room_id, None)
if room is None:
return
room.clients.append(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
@@ -283,13 +288,20 @@ class RoomManager:
await client.send_test_message()
def del_client(self, room_id, client: 'ChatHandler'):
if room_id not in self._rooms:
room = self._rooms.get(room_id, None)
if room is None:
return
room = self._rooms[room_id]
room.clients.remove(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
room.auto_translate_count -= 1
try:
room.clients.remove(client)
except ValueError:
# _add_room未完成没有执行到room.clients.append
pass
else:
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
room.auto_translate_count = max(0, room.auto_translate_count - 1)
if not room.clients:
self._del_room(room_id)
@@ -297,24 +309,25 @@ class RoomManager:
if room_id in self._rooms:
return True
logger.info('Creating room %d', room_id)
room = Room(room_id)
self._rooms[room_id] = room
self._rooms[room_id] = room = Room(room_id)
if await room.init_room():
room.start()
logger.info('%d rooms', len(self._rooms))
return True
else:
self._del_room(room_id)
return False
def _del_room(self, room_id):
if room_id not in self._rooms:
room = self._rooms.get(room_id, None)
if room is None:
return
logger.info('Removing room %d', room_id)
room = self._rooms[room_id]
for client in room.clients:
client.close()
room.stop_and_close()
del self._rooms[room_id]
self._rooms.pop(room_id, None)
logger.info('%d rooms', len(self._rooms))
# noinspection PyAbstractClass
@@ -419,7 +432,9 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
gift_data = {
**base_data,
'id': uuid.uuid4().hex,
'totalCoin': 450000
'totalCoin': 450000,
'giftName': '摩天大楼',
'num': 1
}
sc_data = {
**base_data,
@@ -444,6 +459,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
self.send_message(Command.ADD_GIFT, gift_data)
gift_data['id'] = uuid.uuid4().hex
gift_data['totalCoin'] = 1245000
gift_data['giftName'] = '小电视飞船'
self.send_message(Command.ADD_GIFT, gift_data)
@property
@@ -455,4 +471,4 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
try:
self.write_message(body)
except tornado.websocket.WebSocketClosedError:
pass
self.on_close()

View File

@@ -1,5 +1,13 @@
module.exports = {
presets: [
'@vue/app'
],
plugins: [
[
'component', {
'libraryName': 'element-ui',
'styleLibraryName': 'theme-chalk'
}
]
]
}

15938
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,8 +20,9 @@
"devDependencies": {
"@vue/cli-plugin-babel": "^3.7.0",
"@vue/cli-plugin-eslint": "^3.7.0",
"@vue/cli-service": "^3.7.0",
"@vue/cli-service": "^4.2.2",
"babel-eslint": "^10.0.1",
"babel-plugin-component": "^1.1.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.5.21"

View File

@@ -2,9 +2,11 @@ import {mergeConfig} from '@/utils'
export const DEFAULT_CONFIG = {
minGiftPrice: 7, // $1
mergeSimilarDanmaku: true,
showDanmaku: true,
showGift: true,
showGiftName: false,
mergeSimilarDanmaku: true,
mergeGift: true,
maxNumber: 60,
blockGiftDanmaku: true,

View File

@@ -34,18 +34,19 @@
<paid-message :key="pinnedMessage.id" v-else
class="style-scope yt-live-chat-ticker-renderer"
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName"
:time="pinnedMessage.time" :content="pinnedMessage.content"
:time="pinnedMessage.time" :content="showContent"
></paid-message>
</template>
</yt-live-chat-ticker-renderer>
</template>
<script>
import * as config from '@/api/config'
import {formatCurrency} from '@/utils'
import ImgShadow from './ImgShadow.vue'
import LegacyPaidMessage from './LegacyPaidMessage.vue'
import PaidMessage from './PaidMessage.vue'
import * as constants from './constants'
import {formatCurrency} from '@/utils'
export default {
name: 'Ticker',
@@ -55,7 +56,11 @@ export default {
PaidMessage
},
props: {
messages: Array
messages: Array,
showGiftName: {
type: Boolean,
default: config.DEFAULT_CONFIG.showGiftName
}
},
data() {
return {
@@ -65,6 +70,18 @@ export default {
pinnedMessage: null
}
},
computed: {
showContent() {
if (!this.pinnedMessage) {
return ''
}
if (this.pinnedMessage.type === constants.MESSAGE_TYPE_GIFT) {
return constants.getGiftShowContent(this.pinnedMessage, this.showGiftName)
} else {
return constants.getShowContent(this.pinnedMessage)
}
}
},
beforeDestroy() {
window.clearInterval(this.updateTimerId)
},

View File

@@ -18,10 +18,11 @@ 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
export const MESSAGE_TYPE_UPDATE = 4
export const MESSAGE_TYPE_GIFT = 1
export const MESSAGE_TYPE_MEMBER = 2
export const MESSAGE_TYPE_SUPER_CHAT = 3
export const MESSAGE_TYPE_DEL = 4
export const MESSAGE_TYPE_UPDATE = 5
// 美元 -> 人民币 汇率
const EXCHANGE_RATE = 7
@@ -120,3 +121,17 @@ export function getPriceConfig (price) {
}
return PRICE_CONFIGS[PRICE_CONFIGS.length - 1]
}
export function getShowContent(message) {
if (message.translation) {
return `${message.content}${message.translation}`
}
return message.content
}
export function getGiftShowContent(message, showGiftName) {
if (!showGiftName) {
return ''
}
return `Sent ${message.giftName}x${message.num}`
}

View File

@@ -2,7 +2,9 @@
<yt-live-chat-renderer class="style-scope yt-live-chat-app" style="--scrollbar-width:11px;" hide-timestamps
@mousemove="refreshCantScrollStartTime"
>
<ticker class="style-scope yt-live-chat-renderer" :messages="paidMessages" :hidden="paidMessages.length === 0"></ticker>
<ticker class="style-scope yt-live-chat-renderer" :messages="paidMessages" :showGiftName="showGiftName"
:hidden="paidMessages.length === 0"
></ticker>
<yt-live-chat-item-list-renderer class="style-scope yt-live-chat-renderer" allow-scroll>
<div ref="scroller" id="item-scroller" class="style-scope yt-live-chat-item-list-renderer animated" @scroll="onScroll">
<div ref="itemOffset" id="item-offset" class="style-scope yt-live-chat-item-list-renderer" style="height: 0px;">
@@ -16,6 +18,11 @@
:authorType="message.authorType" :content="getShowContent(message)" :privilegeType="message.privilegeType"
:repeated="message.repeated"
></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"
:time="message.time" :content="getGiftShowContent(message)"
></paid-message>
<legacy-paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-item-list-renderer"
:avatarUrl="message.avatarUrl" :title="message.title" :content="message.content"
@@ -58,6 +65,10 @@ export default {
maxNumber: {
type: Number,
default: config.DEFAULT_CONFIG.maxNumber
},
showGiftName: {
type: Boolean,
default: config.DEFAULT_CONFIG.showGiftName
}
},
data() {
@@ -65,6 +76,7 @@ export default {
document.head.appendChild(styleElement)
return {
MESSAGE_TYPE_TEXT: constants.MESSAGE_TYPE_TEXT,
MESSAGE_TYPE_GIFT: constants.MESSAGE_TYPE_GIFT,
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
MESSAGE_TYPE_SUPER_CHAT: constants.MESSAGE_TYPE_SUPER_CHAT,
@@ -118,12 +130,10 @@ export default {
this.clearMessages()
},
methods: {
getShowContent(message) {
if (message.translation) {
return `${message.content}${message.translation}`
}
return message.content
getGiftShowContent(message) {
return constants.getGiftShowContent(message, this.showGiftName)
},
getShowContent: constants.getShowContent,
addMessage(message) {
this.addMessages([message])
@@ -159,14 +169,15 @@ export default {
})
return res
},
mergeSimilarGift(authorName, price) {
mergeSimilarGift(authorName, price, giftName, num) {
let res = false
this.forEachRecentMessage(5, message => {
if (message.type === constants.MESSAGE_TYPE_SUPER_CHAT
&& message.content === ''
if (message.type === constants.MESSAGE_TYPE_GIFT
&& message.authorName === authorName
&& message.giftName === giftName
) {
message.price += price
message.num += num
res = true
return false
}
@@ -317,6 +328,7 @@ export default {
for (let message of messageGroup) {
switch (message.type) {
case constants.MESSAGE_TYPE_TEXT:
case constants.MESSAGE_TYPE_GIFT:
case constants.MESSAGE_TYPE_MEMBER:
case constants.MESSAGE_TYPE_SUPER_CHAT:
this.handleAddMessage(message)

View File

@@ -14,7 +14,9 @@ export default {
roomId: 'Room ID',
showDanmaku: 'Show messages',
showGift: 'Show Super Chats',
showGiftName: 'Show gift name',
mergeSimilarDanmaku: 'Merge similar messages',
mergeGift: 'Merge gifts',
minGiftPrice: 'Min price of Super Chats to show (CNY)',
maxNumber: 'Max number of messages',

View File

@@ -14,7 +14,9 @@ export default {
roomId: 'ルームID',
showDanmaku: 'コメントを表示する',
showGift: 'スーパーチャットと新メンバーを表示する',
showGiftName: 'ギフト名を表示する',
mergeSimilarDanmaku: '同じコメントを合併する',
mergeGift: 'ギフトを合併する',
minGiftPrice: '最低表示スーパーチャット価格CNY',
maxNumber: '最大コメント数',
@@ -28,7 +30,7 @@ export default {
blockUsers: 'ブロックユーザー',
blockMedalLevel: 'ブロック勲章等級がx未満',
advanced: 'アドバンス',
advanced: 'アドバンス',
autoTranslate: '自動翻訳コメントから日本語へ',
roomUrl: 'ルームのURL',

View File

@@ -14,7 +14,9 @@ export default {
roomId: '房间ID',
showDanmaku: '显示弹幕',
showGift: '显示打赏和新舰长',
showGiftName: '显示礼物名',
mergeSimilarDanmaku: '合并相似弹幕',
mergeGift: '合并礼物',
minGiftPrice: '最低显示打赏价格(元)',
maxNumber: '最大弹幕数',

View File

@@ -9,7 +9,7 @@
</router-link>
</div>
<div class="version">
v1.4.0
v1.4.2
</div>
<sidebar></sidebar>
</el-aside>

View File

@@ -1,8 +1,10 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
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
} from 'element-ui'
import axios from 'axios'
import App from './App.vue'
@@ -23,8 +25,32 @@ if (process.env.NODE_ENV === 'development') {
}
Vue.use(VueRouter)
Vue.use(ElementUI)
Vue.use(VueI18n)
// 初始化element
Vue.use(Aside)
Vue.use(Autocomplete)
Vue.use(Badge)
Vue.use(Button)
Vue.use(Col)
Vue.use(ColorPicker)
Vue.use(Container)
Vue.use(Divider)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Image)
Vue.use(Input)
Vue.use(Main)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Row)
Vue.use(Scrollbar)
Vue.use(Slider)
Vue.use(Submenu)
Vue.use(Switch)
Vue.use(TabPane)
Vue.use(Tabs)
Vue.use(Tooltip)
Vue.prototype.$message = Message
Vue.config.ignoredElements = [
/^yt-/

View File

@@ -16,9 +16,15 @@
<el-form-item :label="$t('home.showGift')">
<el-switch v-model="form.showGift"></el-switch>
</el-form-item>
<el-form-item :label="$t('home.showGiftName')">
<el-switch v-model="form.showGiftName"></el-switch>
</el-form-item>
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
</el-form-item>
<el-form-item :label="$t('home.mergeGift')">
<el-switch v-model="form.mergeGift"></el-switch>
</el-form-item>
<el-form-item :label="$t('home.minGiftPrice')">
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
</el-form-item>

View File

@@ -1,5 +1,5 @@
<template>
<chat-renderer ref="renderer" :maxNumber="config.maxNumber"></chat-renderer>
<chat-renderer ref="renderer" :maxNumber="config.maxNumber" :showGiftName="config.showGiftName"></chat-renderer>
</template>
<script>
@@ -43,6 +43,11 @@ export default {
created() {
this.updateConfig()
this.wsConnect()
// 提示用户已加载
this.$message({
message: 'Loaded',
duration: '500'
})
},
beforeDestroy() {
this.isDestroying = true
@@ -60,9 +65,11 @@ export default {
cfg = mergeConfig(cfg, config.DEFAULT_CONFIG)
cfg.minGiftPrice = toInt(cfg.minGiftPrice, config.DEFAULT_CONFIG.minGiftPrice)
cfg.mergeSimilarDanmaku = toBool(cfg.mergeSimilarDanmaku)
cfg.showDanmaku = toBool(cfg.showDanmaku)
cfg.showGift = toBool(cfg.showGift)
cfg.showGiftName = toBool(cfg.showGiftName)
cfg.mergeSimilarDanmaku = toBool(cfg.mergeSimilarDanmaku)
cfg.mergeGift = toBool(cfg.mergeGift)
cfg.maxNumber = toInt(cfg.maxNumber, config.DEFAULT_CONFIG.maxNumber)
cfg.blockGiftDanmaku = toBool(cfg.blockGiftDanmaku)
cfg.blockLevel = toInt(cfg.blockLevel, config.DEFAULT_CONFIG.blockLevel)
@@ -153,20 +160,21 @@ export default {
break
}
let price = data.totalCoin / 1000
if (price < this.config.minGiftPrice) { // 丢人
if (this.mergeSimilarGift(data.authorName, price, data.giftName, data.num)) {
break
}
if (this.mergeSimilarGift(data.authorName, price)) {
if (price < this.config.minGiftPrice) { // 丢人
break
}
message = {
id: data.id,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
type: constants.MESSAGE_TYPE_GIFT,
avatarUrl: data.avatarUrl,
time: new Date(data.timestamp * 1000),
authorName: data.authorName,
price: price,
time: new Date(data.timestamp * 1000),
content: '' // 有了SC礼物不需要内容了
giftName: data.giftName,
num: data.num
}
break
}
@@ -257,11 +265,11 @@ export default {
}
return this.$refs.renderer.mergeSimilarText(content)
},
mergeSimilarGift(authorName, price) {
if (!this.config.mergeSimilarDanmaku) {
mergeSimilarGift(authorName, price, giftName, num) {
if (!this.config.mergeGift) {
return false
}
return this.$refs.renderer.mergeSimilarGift(authorName, price)
return this.$refs.renderer.mergeSimilarGift(authorName, price, giftName, num)
}
}
}

View File

@@ -268,8 +268,8 @@ const EXAMPLE_MESSAGES = [
{
...legacyPaidMessageTemplate,
id: (nextId++).toString(),
authorName: '少年Pi',
content: 'Welcome 少年Pi!'
authorName: '进击的冰糖',
content: 'Welcome 进击的冰糖!'
},
{
...paidMessageTemplate,

View File

@@ -174,9 +174,8 @@ def update_avatar_cache(user_id, avatar_url):
def _update_avatar_cache_in_memory(user_id, avatar_url):
_avatar_url_cache[user_id] = avatar_url
if len(_avatar_url_cache) > 50000:
for _, key in zip(range(100), _avatar_url_cache):
del _avatar_url_cache[key]
while len(_avatar_url_cache) > 50000:
_avatar_url_cache.pop(next(iter(_avatar_url_cache)), None)
def _update_avatar_cache_in_database(user_id, avatar_url):

View File

@@ -16,7 +16,8 @@ DbSession: Optional[Type[sqlalchemy.orm.Session]] = None
def init(debug):
cfg = config.get_config()
global engine, DbSession
engine = sqlalchemy.create_engine(cfg.database_url, echo=debug)
# engine = sqlalchemy.create_engine(cfg.database_url, echo=debug)
engine = sqlalchemy.create_engine(cfg.database_url)
DbSession = sqlalchemy.orm.sessionmaker(bind=engine)
OrmBase.metadata.create_all(engine)

View File

@@ -103,9 +103,8 @@ def _on_translate_done(key, future):
if res is None:
return
_translate_cache[key] = res
if len(_translate_cache) > 50000:
for _, key in zip(range(100), _translate_cache):
del _translate_cache[key]
while len(_translate_cache) > 50000:
_translate_cache.pop(next(iter(_translate_cache)), None)
class TranslateProvider:
@@ -122,13 +121,19 @@ class TranslateProvider:
class TencentTranslate(TranslateProvider):
def __init__(self):
# 过期时间1小时
self._qtv = ''
self._qtk = ''
self._reinit_future = None
# 连续失败的次数
self._fail_count = 0
self._cool_down_future = None
async def init(self):
if self._reinit_future is not None:
self._reinit_future.cancel()
self._reinit_future = None
try:
async with _http_session.get('https://fanyi.qq.com/') as r:
if r.status != 200:
@@ -143,14 +148,25 @@ class TencentTranslate(TranslateProvider):
if m is None:
logger.exception('TencentTranslate init failed: qtv not found')
return False
self._qtv = m[1]
qtv = m[1]
m = re.search(r"""\bqtk\s*=\s*['"](.+?)['"]""", html)
if m is None:
logger.exception('TencentTranslate init failed: qtk not found')
return False
self._qtk = m[1]
qtk = m[1]
self._qtv = qtv
self._qtk = qtk
self._reinit_future = asyncio.ensure_future(self._reinit_coroutine())
return True
async def _reinit_coroutine(self):
try:
await asyncio.sleep(55 * 60)
await self.init()
except asyncio.CancelledError:
pass
@property
def is_available(self):
return self._qtv != '' and self._qtk != ''
@@ -195,7 +211,12 @@ class TencentTranslate(TranslateProvider):
if data['errCode'] != 0:
logger.warning('TencentTranslate failed: %d %s', data['errCode'], data['errMsg'])
return None
return ''.join(record['targetText'] for record in data['translate']['records'])
res = ''.join(record['targetText'] for record in data['translate']['records'])
if res == '' and text.strip() != '':
# qtv、qtk过期
logger.warning('TencentTranslate result is empty %s', data)
return None
return res
def _on_fail(self):
self._fail_count += 1
@@ -210,7 +231,7 @@ class TencentTranslate(TranslateProvider):
while True:
await asyncio.sleep(3 * 60)
try:
is_success = self.init()
is_success = await self.init()
except:
logger.exception('TencentTranslate init error:')
continue
@@ -320,7 +341,7 @@ class YoudaoTranslate(TranslateProvider):
while True:
await asyncio.sleep(3 * 60)
try:
is_success = self.init()
is_success = await self.init()
except:
logger.exception('YoudaoTranslate init error:')
continue

View File

@@ -4,7 +4,7 @@ import asyncio
import aiohttp
VERSION = 'v1.4.0'
VERSION = 'v1.4.2'
def check_update():