Compare commits

..

43 Commits

Author SHA1 Message Date
John Smith
9565c52e23 更新版本号 2019-10-13 21:19:33 +08:00
John Smith
bb1acaecd6 配置允许储存任意JSON 2019-10-13 21:04:03 +08:00
John Smith
dcbb90a7bd 兼容B站弹幕升级 2019-10-13 20:27:52 +08:00
John Smith
c46542bee0 兼容HTTPS 2019-10-13 17:02:54 +08:00
John Smith
709ec0585e 更新README、版本号 2019-10-06 16:56:40 +08:00
John Smith
a9c6709080 添加BiliSC链接 2019-10-06 16:41:47 +08:00
John Smith
938f758920 添加掉线重连 2019-10-06 16:20:48 +08:00
John Smith
ce4974be94 添加删除SC功能 2019-10-06 12:23:47 +08:00
John Smith
040bad0eba 修复空SC下面不是圆角边框的问题 2019-10-06 10:55:01 +08:00
John Smith
60e6b5ec99 隐藏空SC内容、更新版本号 2019-09-23 22:28:11 +08:00
John Smith
695a111c9a SC使用官方功能 2019-09-23 22:22:27 +08:00
John Smith
0160549dbe 重复标记用HSL、代码优化 2019-09-22 17:18:34 +08:00
John Smith
3fd0d9ab28 更新JS包 2019-09-17 23:53:26 +08:00
John Smith
b1061ae103 更新blivedm 2019-09-17 23:08:03 +08:00
John Smith
6e737ce33f 样式生成器添加梗 2019-09-16 22:21:44 +08:00
John Smith
10db6b9ae2 添加最大弹幕数 2019-09-16 22:15:32 +08:00
John Smith
1514bfed5b 自动检查更新 2019-09-16 20:48:03 +08:00
John Smith
dc264d40f0 SC配置按美元汇率 2019-09-15 23:31:23 +08:00
John Smith
104e647f61 样式生成器添加默认字体 2019-07-18 09:15:27 +08:00
John Smith
d5d7fdde6d 日志改用英语 2019-07-18 09:08:03 +08:00
John Smith
23aa88d564 时间补齐2位 2019-07-18 08:31:58 +08:00
John Smith
ac1bda0392 修改一堆文案 2019-07-15 21:30:52 +08:00
John Smith
5cdb786966 去掉了点击预览会消失的CSS 2019-07-15 19:50:51 +08:00
John Smith
2b5ca2e219 Merge pull request #8 from simon300000/honest
「尝试」优化了样式生成器的CSS,加入了一个只有好孩子才能看到的小按钮
2019-07-15 19:30:39 +08:00
John Smith
fc77b246c2 我傻了 2019-07-15 18:01:38 +08:00
John Smith
083832cb3e Merge pull request #7 from simon300000/patch-css
增加element字体CSS
2019-07-15 17:58:42 +08:00
John Smith
804db2fec1 全局应用字体 2019-07-15 17:56:03 +08:00
simon3000
769964d5db 增加element字体CSS 2019-07-15 17:56:03 +08:00
John Smith
aba0efca34 Merge pull request #9 from xfgryujk/dependabot/npm_and_yarn/frontend/lodash-4.17.13
Bump lodash from 4.17.11 to 4.17.13 in /frontend
2019-07-15 17:35:06 +08:00
dependabot[bot]
9ff18fe8b5 Bump lodash from 4.17.11 to 4.17.13 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.13.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.13)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-15 09:33:24 +00:00
simon3000
ee78cbf90e 改用refs直接执行function 2019-07-13 01:51:58 +02:00
simon3000
ffc3c9d8a0 将监听的event名称改为exampleResize 2019-07-13 01:16:57 +02:00
simon3000
a69b6b6406 给ChatRenderer加上demo prop, 为true时自动resize 2019-07-13 01:14:55 +02:00
simon3000
1252402e5e Merge branch 'patch-scroll-on-resize' into honest 2019-07-13 01:03:45 +02:00
simon3000
53be587189 自动resize 2019-07-11 23:14:11 +02:00
simon3000
d3cc8b0c77 👿 2019-07-11 22:52:06 +02:00
simon3000
4f4d117ce0 使用calc优化CSS生成器的浏览部分 2019-07-11 22:51:41 +02:00
simon3000
0d1e38346e 增加element字体CSS 2019-07-11 22:15:46 +02:00
simon3000
273dc0f43f 窗口缩放时滚动 2019-07-11 21:50:28 +02:00
John Smith
26f19b6630 Update README.md 2019-07-11 11:35:17 +08:00
John Smith
f2d2528afe 修改默认端口为12450 2019-07-10 15:30:35 +08:00
John Smith
d0fd997415 兼容B站弹幕升级 2019-07-10 15:19:03 +08:00
John Smith
7086acb001 补i18n依赖 2019-07-08 16:45:14 +08:00
31 changed files with 18882 additions and 6297 deletions

View File

@@ -28,6 +28,6 @@ RUN npm run build
# 运行
WORKDIR /blivechat
EXPOSE 80
EXPOSE 12450
ENTRYPOINT ["python3", "main.py"]
CMD ["--host", "0.0.0.0", "--port", "80"]
CMD ["--host", "0.0.0.0", "--port", "12450"]

View File

@@ -1,5 +1,5 @@
# blivechat
用于OBS的仿YouTube风格的bilibili直播聊天层
用于OBS的仿YouTube风格的bilibili直播评论栏
最近喜欢看VTuber想为此写些程序于是有了这个东西。~~写到一半发现有类似项目了:[bilibili-live-chat](https://github.com/Tsuk1ko/bilibili-live-chat)、[BiliChat](https://github.com/3Shain/BiliChat)。~~ 本项目就当做练手吧而且对YouTube的模仿程度更高
@@ -8,23 +8,29 @@
![样式生成器截图](https://github.com/xfgryujk/blivechat/blob/master/screenshots/stylegen.png)
## 特性
* 兼容YouTube直播chat的样式
* 金瓜子礼物模仿Super Chat显示
* 兼容YouTube直播评论栏的样式
* 金瓜子礼物模仿醒目留言显示
* 高亮舰队、房管、主播的用户名
* 支持屏蔽弹幕、限制最大速度等设置
* 自带样式生成器
## 使用方法
### 发布版
### 本地使用
1. 下载[发布版](https://github.com/xfgryujk/blivechat/releases)仅提供x64 Windows版
2. 双击`blivechat.exe`运行服务器或者用命令行可以指定host和端口号
```bat
blivechat.exe --host 127.0.0.1 --port 80
blivechat.exe --host 127.0.0.1 --port 12450
```
3. 用浏览器打开[http://localhost](http://localhost)输入房间ID保存配置进入房间,复制房间URL
4. (可选)用样式生成器生成样式复制CSS
3. 用浏览器打开[http://localhost:12450](http://localhost:12450)输入房间ID保存配置复制房间URL
4. 用样式生成器生成样式复制CSS
5. 在OBS中添加浏览器源输入URL和自定义CSS或者可以在首页的样式设置里输入CSS
### 公共服务器
请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能出现直播事故
* [第三方公共服务器](http://chat.bilisc.com/)
* [仅样式生成器](https://style.vtbs.moe/)
### 源代码版
1. 编译前端需要安装Node.js和npm
```sh
@@ -39,14 +45,12 @@
```
或者可以指定host和端口号
```sh
python3 main.py --host 127.0.0.1 --port 80
python3 main.py --host 127.0.0.1 --port 12450
```
3. 用浏览器打开[http://localhost](http://localhost)输入房间ID保存配置进入房间复制房间URL
4. 可选用样式生成器生成样式复制CSS
5. 在OBS中添加浏览器源输入URL和自定义CSS或者可以在首页的样式设置里输入CSS
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
### Docker
1. ```sh
docker run -d -p 80:80 xfgryujk/blivechat
docker run -d -p 12450:12450 xfgryujk/blivechat:latest
```
2. 以下略
2. 用浏览器打开[http://localhost:12450](http://localhost:12450)以下略

Submodule blivedm updated: 003d89e976...efc7f3b3f8

24493
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,9 @@
"core-js": "^2.6.5",
"downloadjs": "^1.4.7",
"element-ui": "^2.9.1",
"lodash": "^4.17.11",
"lodash": "^4.17.13",
"vue": "^2.6.10",
"vue-i18n": "^8.11.2",
"vue-router": "^3.0.6"
},
"devDependencies": {

View File

@@ -1,14 +1,14 @@
import axios from 'axios'
import {mergeConfig} from '@/utils'
import stylegen from '@/views/StyleGenerator/stylegen'
export const DEFAULT_CONFIG = {
minGiftPrice: 6.911, // $1
minGiftPrice: 7, // $1
mergeSimilarDanmaku: true,
showDanmaku: true,
showGift: true,
maxSpeed: 4,
maxNumber: 60,
blockGiftDanmaku: true,
blockLevel: 0,
@@ -18,7 +18,7 @@ export const DEFAULT_CONFIG = {
blockUsers: '',
blockMedalLevel: 0,
css: stylegen.getStyle(stylegen.DEFAULT_CONFIG)
css: ''
}
export function setLocalConfig (config) {

View File

@@ -10,7 +10,7 @@
<div id="event-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{title}}</div>
<div id="detail-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{content}}</div>
</div>
<div id="timestamp" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{time}}</div>
<div id="timestamp" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{timeText}}</div>
</div>
</div>
<div id="inline-action-button-container" class="style-scope yt-live-chat-legacy-paid-message-renderer" aria-hidden="true">
@@ -21,6 +21,7 @@
<script>
import ImgShadow from './ImgShadow.vue'
import utils from '@/utils'
export default {
name: 'LegacyPaidMessage',
@@ -32,7 +33,12 @@ export default {
authorName: String,
title: String,
content: String,
time: String
time: Date
},
computed: {
timeText() {
return utils.getTimeTextMinSec(this.time)
}
}
}
</script>

View File

@@ -1,5 +1,6 @@
<template>
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations :style="{
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations
:show-only-header="!content" :style="{
'--yt-live-chat-paid-message-primary-color': color.contentBg,
'--yt-live-chat-paid-message-secondary-color': color.headerBg,
'--yt-live-chat-paid-message-header-color': color.header,
@@ -16,9 +17,9 @@
<div id="header-content" class="style-scope yt-live-chat-paid-message-renderer">
<div id="header-content-primary-column" class="style-scope yt-live-chat-paid-message-renderer">
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{authorName}}</div>
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">CN¥{{price}}</div>
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">{{priceText}}</div>
</div>
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{time}}</span>
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{timeText}}</span>
</div>
</div>
<div id="content" class="style-scope yt-live-chat-paid-message-renderer">
@@ -33,6 +34,7 @@
<script>
import ImgShadow from './ImgShadow.vue'
import * as constants from './constants'
import utils from '@/utils'
export default {
name: 'PaidMessage',
@@ -43,12 +45,18 @@ export default {
avatarUrl: String,
authorName: String,
price: Number, // 价格,人民币
time: String,
time: Date,
content: String
},
computed: {
color() {
return constants.getPriceConfig(this.price).colors
},
priceText() {
return 'CN¥' + utils.formatCurrency(this.price)
},
timeText() {
return utils.getTimeTextMinSec(this.time)
}
}
}

View File

@@ -4,7 +4,7 @@
:imgUrl="avatarUrl"
></img-shadow>
<div id="content" class="style-scope yt-live-chat-text-message-renderer">
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{time}}</span>
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{timeText}}</span>
<yt-live-chat-author-chip class="style-scope yt-live-chat-text-message-renderer">
<span id="author-name" dir="auto" class="style-scope yt-live-chat-author-chip" :type="authorTypeText">
{{authorName}}
@@ -29,9 +29,11 @@
import ImgShadow from './ImgShadow.vue'
import AuthorBadge from './AuthorBadge.vue'
import * as constants from './constants'
import utils from '@/utils'
const REPEATED_MARK_COLOR_START = [64, 158, 255]
const REPEATED_MARK_COLOR_END = [245, 108, 108]
// HSL
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
export default {
name: 'TextMessage',
@@ -41,7 +43,7 @@ export default {
},
props: {
avatarUrl: String,
time: String,
time: Date,
authorName: String,
authorType: Number,
content: String,
@@ -49,6 +51,9 @@ export default {
repeated: Number
},
computed: {
timeText() {
return utils.getTimeTextMinSec(this.time)
},
authorTypeText() {
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
},
@@ -65,7 +70,7 @@ export default {
color[i] = REPEATED_MARK_COLOR_START[i] + (REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t
}
}
return `rgb(${color.join(', ')})`
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
}
}
}

View File

@@ -45,6 +45,7 @@ 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',
@@ -106,9 +107,9 @@ export default {
},
getText(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return '会员'
return 'Member'
}
return `CN¥${message.price}`
return 'CN¥' + formatCurrency(message.price)
},
updateProgress() {
let curTime = new Date()

View File

@@ -19,11 +19,13 @@ export const GUARD_LEVEL_TO_TEXT = [
export const MESSAGE_TYPE_TEXT = 0
export const MESSAGE_TYPE_MEMBER = 1
export const MESSAGE_TYPE_GIFT = 2
export const MESSAGE_TYPE_SUPER_CHAT = 2
// 美元 -> 人民币 汇率
const EXCHANGE_RATE = 7
export const PRICE_CONFIGS = [
{ // $100红
price: 1245, // >= 1245小电视飞船
price: 100 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(230,33,23,1)',
headerBg: 'rgba(208,0,0,1)',
@@ -35,7 +37,7 @@ export const PRICE_CONFIGS = [
pinTime: 60
},
{ // $50品红
price: 450, // 450~1245摩天大楼
price: 50 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(233,30,99,1)',
headerBg: 'rgba(194,24,91,1)',
@@ -47,7 +49,7 @@ export const PRICE_CONFIGS = [
pinTime: 30
},
{ // $20橙
price: 100, // 100~450节奏风暴、天空之翼
price: 20 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(245,124,0,1)',
headerBg: 'rgba(230,81,0,1)',
@@ -59,7 +61,7 @@ export const PRICE_CONFIGS = [
pinTime: 10
},
{ // $10黄
price: 52, // 52~100疯狂打call
price: 10 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(255,202,40,1)',
headerBg: 'rgba(255,179,0,1)',
@@ -71,7 +73,7 @@ export const PRICE_CONFIGS = [
pinTime: 5
},
{ // $5绿
price: 28, // 28~52礼花
price: 5 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(29,233,182,1)',
headerBg: 'rgba(0,191,165,1)',
@@ -83,7 +85,7 @@ export const PRICE_CONFIGS = [
pinTime: 2
},
{ // $2浅蓝
price: 9.9, // 9.9~28B坷垃
price: 2 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(0,229,255,1)',
headerBg: 'rgba(0,184,212,1)',
@@ -95,7 +97,7 @@ export const PRICE_CONFIGS = [
pinTime: 0
},
{ // $1蓝
price: 0, // 0~9.9,丢人
price: 1 * EXCHANGE_RATE,
colors: {
contentBg: 'rgba(30,136,229,1)',
headerBg: 'rgba(21,101,192,1)',

View File

@@ -37,6 +37,9 @@ import LegacyPaidMessage from './LegacyPaidMessage.vue'
import PaidMessage from './PaidMessage.vue'
import * as constants from './constants'
// const CHAT_SMOOTH_ANIMATION_TIME_MS = 84
const SCROLLED_TO_BOTTOM_EPSILON = 15
export default {
name: 'ChatRenderer',
components: {
@@ -83,8 +86,8 @@ export default {
this.$refs.scroller.scrollTop = this.$refs.scroller.scrollHeight
},
onScroll() {
this.canAutoScroll = Math.abs(this.$refs.scroller.scrollHeight - this.$refs.scroller.scrollTop
- this.$refs.scroller.clientHeight) < 2
let scroller = this.$refs.scroller
this.canAutoScroll = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON
}
}
}

View File

@@ -2,7 +2,8 @@ export default {
sidebar: {
home: 'Home',
stylegen: 'Style generator',
projectAddress: 'Project address'
projectAddress: 'Project address',
giftRecord: 'Super Chat record'
},
home: {
roomIdEmpty: "Room ID can't be empty",
@@ -15,16 +16,17 @@ export default {
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',
giftDanmaku: 'System messages (gift effect)',
blockLevel: 'User level lower than',
informalUser: 'Informal users',
unverifiedUser: 'Unverified users',
giftDanmaku: 'Block system messages (gift effect)',
blockLevel: 'Block user level lower than',
informalUser: 'Block informal users',
unverifiedUser: 'Block unverified users',
blockKeywords: 'Block keywords',
onePerLine: 'One per line',
blockUsers: 'Block users',
blockMedalLevel: 'Medal level lower than',
blockMedalLevel: 'Block medal level lower than',
style: 'Style',

View File

@@ -2,7 +2,8 @@
sidebar: {
home: 'トップページ',
stylegen: 'スタイルジェネレータ',
projectAddress: 'プロジェクトアドレス'
projectAddress: 'プロジェクトアドレス',
giftRecord: 'スーパーチャット記録'
},
home: {
roomIdEmpty: 'ルームのIDを空白にすることはできません',
@@ -15,16 +16,17 @@
mergeSimilarDanmaku: '同じコメントを合併する',
minGiftPrice: '最低表示スーパーチャット価格CNY',
maxSpeed: 'コメントの最大速度(バー/秒、0は無制限',
maxNumber: '最大コメント数',
block: 'ブロック',
giftDanmaku: 'システムコメント(プレゼント効果)',
blockLevel: 'ユーザーレベルがx未満',
informalUser: '非公式ユーザー',
unverifiedUser: '認証されていないユーザー',
blockKeywords: 'キーワード',
giftDanmaku: 'ブロックシステムコメント(プレゼント効果)',
blockLevel: 'ブロックユーザーレベルがx未満',
informalUser: 'ブロック非公式ユーザー',
unverifiedUser: 'ブロック認証されていないユーザー',
blockKeywords: 'ブロックキーワード',
onePerLine: '1行に1つずつ',
blockUsers: 'ユーザー',
blockMedalLevel: '勲章等級がx未満',
blockUsers: 'ブロックユーザー',
blockMedalLevel: 'ブロック勲章等級がx未満',
style: 'スタイル',

View File

@@ -2,7 +2,8 @@ export default {
sidebar: {
home: '首页',
stylegen: '样式生成器',
projectAddress: '项目地址'
projectAddress: '项目地址',
giftRecord: '打赏记录'
},
home: {
roomIdEmpty: '房间ID不能为空',
@@ -11,20 +12,21 @@ export default {
general: '常规',
roomId: '房间ID',
showDanmaku: '显示弹幕',
showGift: '显示礼物和新舰长',
showGift: '显示打赏和新舰长',
mergeSimilarDanmaku: '合并相似弹幕',
minGiftPrice: '最低显示礼物价格(元)',
minGiftPrice: '最低显示打赏价格(元)',
maxSpeed: '弹幕最大速度(条/秒0为无限制',
maxNumber: '最大弹幕数',
block: '屏蔽',
giftDanmaku: '礼物弹幕',
blockLevel: '用户等级低于',
informalUser: '非正式会员',
unverifiedUser: '未绑定手机用户',
giftDanmaku: '屏蔽礼物弹幕',
blockLevel: '屏蔽用户等级低于',
informalUser: '屏蔽非正式会员',
unverifiedUser: '屏蔽未绑定手机用户',
blockKeywords: '屏蔽关键词',
onePerLine: '一行一个',
blockUsers: '屏蔽用户',
blockMedalLevel: '当前直播间勋章等级低于',
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
style: '样式',
@@ -75,7 +77,7 @@ export default {
moderatorMessageBgColor: '房管消息背景色',
memberMessageBgColor: '舰长消息背景色',
scAndNewMember: '礼物、舰长',
scAndNewMember: '打赏、舰长',
firstLineFont: '第一行字体',
firstLineFontSize: '第一行字体尺寸',
firstLineLineHeight: '第一行行高0为默认',

View File

@@ -18,6 +18,11 @@
<i class="el-icon-share"></i>{{$t('sidebar.projectAddress')}}
</el-menu-item>
</a>
<a href="https://bilisc.com/" target="_blank">
<el-menu-item>
<i class="el-icon-share"></i>{{$t('sidebar.giftRecord')}}
</el-menu-item>
</a>
<el-submenu index="null">
<template slot="title">
<i class="el-icon-chat-line-square"></i>Language

View File

@@ -47,6 +47,10 @@ export default {
</script>
<style>
html {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html, body, #app, .app-wrapper, .sidebar-container {
height: 100%;
}

View File

@@ -17,8 +17,8 @@ import ja from './lang/ja'
import en from './lang/en'
if (process.env.NODE_ENV === 'development') {
// 开发时使用localhost:80
axios.defaults.baseURL = 'http://localhost'
// 开发时使用localhost:12450
axios.defaults.baseURL = 'http://localhost:12450'
}
Vue.use(VueRouter)

View File

@@ -6,6 +6,20 @@ export function mergeConfig (config, defaultConfig) {
return res
}
export default {
mergeConfig
export function formatCurrency (price) {
return new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: price < 100 ? 2 : 0
}).format(price)
}
export function getTimeTextMinSec (date) {
let min = ('00' + date.getMinutes()).slice(-2)
let sec = ('00' + date.getSeconds()).slice(-2)
return `${min}:${sec}`
}
export default {
mergeConfig,
formatCurrency,
getTimeTextMinSec
}

View File

@@ -25,6 +25,9 @@
<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>
</el-tab-pane>
<el-tab-pane :label="$t('home.block')">

View File

@@ -7,10 +7,13 @@ import config from '@/api/config'
import ChatRenderer from '@/components/ChatRenderer'
import * as constants from '@/components/ChatRenderer/constants'
const COMMAND_JOIN_ROOM = 0
const COMMAND_ADD_TEXT = 1
const COMMAND_ADD_GIFT = 2
const COMMAND_ADD_MEMBER = 3
const COMMAND_HEARTBEAT = 0
const COMMAND_JOIN_ROOM = 1
const COMMAND_ADD_TEXT = 2
const COMMAND_ADD_GIFT = 3
const COMMAND_ADD_MEMBER = 4
const COMMAND_ADD_SUPER_CHAT = 5
const COMMAND_DEL_SUPER_CHAT = 6
export default {
name: 'Room',
@@ -24,7 +27,12 @@ export default {
cfg.maxSpeed = 0
return {
config: cfg,
websocket: null,
retryCount: 0,
isDestroying: false,
heartbeatTimerId: null,
messagesBufferTimerId: null,
nextId: 0,
messagesBuffer: [], // 暂时不显示的消息,可能会丢弃
@@ -32,28 +40,17 @@ export default {
paidMessages: []
}
},
async created() {
// 开发时使用localhost:80
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost/chat' : `ws://${window.location.host}/chat`
this.websocket = new WebSocket(url)
this.websocket.onopen = this.onWsOpen.bind(this)
this.websocket.onmessage = this.onWsMessage.bind(this)
created() {
this.wsConnect()
if (this.$route.query.config_id) {
try {
let cfg = await config.getRemoteConfig(this.$route.query.config_id)
cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
this.config = cfg
} catch (e) {
this.$message.error('获取配置失败:' + e)
}
this.updateConfig(this.$route.query.config_id)
}
},
beforeDestroy() {
if (this.messagesBufferTimerId) {
window.clearInterval(this.messagesBufferTimerId)
}
this.isDestroying = true
this.websocket.close()
},
watch: {
@@ -63,12 +60,39 @@ export default {
this.messagesBufferTimerId = null
}
if (val.maxSpeed > 0) {
this.messagesBufferTimerId = window.setInterval(this.handleMessagesBuffer.bind(this), 1000 / val.maxSpeed)
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
} catch (e) {
this.$message.error('获取配置失败:' + e)
}
},
wsConnect() {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
// 开发时使用localhost:12450
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
const url = `${protocol}://${host}/chat`
this.websocket = new WebSocket(url)
this.websocket.onopen = this.onWsOpen
this.websocket.onclose = this.onWsClose
this.websocket.onmessage = this.onWsMessage
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat, 10 * 1000)
},
sendHeartbeat() {
this.websocket.send(JSON.stringify({
cmd: COMMAND_HEARTBEAT
}))
},
onWsOpen() {
this.retryCount = 0
this.websocket.send(JSON.stringify({
cmd: COMMAND_JOIN_ROOM,
data: {
@@ -76,19 +100,31 @@ export default {
}
}))
},
onWsClose() {
if (this.heartbeatTimerId) {
window.clearInterval(this.heartbeatTimerId)
this.heartbeatTimerId = null
}
if (this.isDestroying) {
return
}
window.console.log(`掉线重连中${++this.retryCount}`)
this.wsConnect()
},
onWsMessage(event) {
let {cmd, data} = JSON.parse(event.data)
let message = null
let time = data.timestamp ? new Date(data.timestamp * 1000) : new Date()
switch(cmd) {
switch (cmd) {
case COMMAND_ADD_TEXT:
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilar(data.content)) {
break
}
message = {
id: `text_${this.nextId++}`,
type: constants.MESSAGE_TYPE_TEXT,
avatarUrl: data.avatarUrl,
time: `${time.getMinutes()}:${time.getSeconds()}`,
time: time,
authorName: data.authorName,
authorType: data.authorType,
content: data.content,
@@ -101,31 +137,65 @@ export default {
break
}
let price = data.totalCoin / 1000
if (price < this.config.minGiftPrice) // 丢人
if (price < this.config.minGiftPrice) { // 丢人
break
}
message = {
type: constants.MESSAGE_TYPE_GIFT,
id: `gift_${this.nextId++}`,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
price: price,
time: `${time.getMinutes()}:${time.getSeconds()}`,
content: `Sent ${data.giftName}x${data.giftNum}`
time: time,
content: '' // 有了SC礼物不需要内容了
}
break
}
case COMMAND_ADD_MEMBER:
if (!this.config.showGift) {
if (!this.config.showGift || !this.filterSuperChatMessage(data)) {
break
}
message = {
id: `member_${this.nextId++}`,
type: constants.MESSAGE_TYPE_MEMBER,
avatarUrl: data.avatarUrl,
time: `${time.getMinutes()}:${time.getSeconds()}`,
time: time,
authorName: data.authorName,
title: 'NEW MEMBER!',
content: `Welcome ${data.authorName}`
content: `Welcome ${data.authorName}!`
}
break
case COMMAND_ADD_SUPER_CHAT:
if (!this.config.showGift) {
break
}
if (data.price < this.config.minGiftPrice) { // 丢人
break
}
message = {
id: `sc_${data.id}`,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
price: data.price,
time: time,
content: data.content
}
break
case COMMAND_DEL_SUPER_CHAT: {
let arrays = [this.messages, this.messagesBuffer, this.paidMessages]
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)
}
}
}
}
break
}
}
if (message) {
this.addMessageBuffer(message)
@@ -155,6 +225,19 @@ export default {
}
return true
},
filterSuperChatMessage(data) {
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
},
mergeSimilar(content) {
if (!this.config.mergeSimilarDanmaku) {
return false
@@ -172,7 +255,7 @@ export default {
return false
},
addMessageBuffer(message) {
if (this.config.maxSpeed > 0 && message.type === 0) {
if (this.config.maxSpeed > 0 && message.type === constants.MESSAGE_TYPE_TEXT) {
message.addTime = new Date()
this.messagesBuffer.push(message)
} else {
@@ -181,7 +264,6 @@ export default {
}
},
addMessageShow(message) {
message.id = this.nextId++
message.addTime = new Date()
this.messages.push(message)
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
@@ -189,8 +271,8 @@ export default {
}
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
this.$nextTick(() => {
if (this.messages.length > 50) {
this.messages.splice(0, this.messages.length - 50)
if (this.messages.length > this.config.maxNumber) {
this.messages.splice(0, this.messages.length - this.config.maxNumber)
}
})
},

View File

@@ -188,9 +188,9 @@
</el-form>
</el-col>
<el-col :span="12">
<div id="example-container">
<div id="example-container" ref="exampleContainer">
<div id="fakebody">
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss"></chat-renderer>
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss" ref="example"></chat-renderer>
</div>
</div>
</el-col>
@@ -211,7 +211,7 @@ let textMessageTemplate = {
addTime: time,
type: constants.MESSAGE_TYPE_TEXT,
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
time: `${time.getMinutes()}:${time.getSeconds()}`,
time: time,
authorName: '',
authorType: constants.AUTHRO_TYPE_NORMAL,
content: '',
@@ -223,7 +223,7 @@ let legacyPaidMessageTemplate = {
addTime: time,
type: constants.MESSAGE_TYPE_MEMBER,
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
time: `${time.getMinutes()}:${time.getSeconds()}`,
time: time,
authorName: '',
title: 'NEW MEMBER!',
content: ''
@@ -231,11 +231,11 @@ let legacyPaidMessageTemplate = {
let paidMessageTemplate = {
id: 0,
addTime: time,
type: constants.MESSAGE_TYPE_GIFT,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
authorName: '',
price: 0,
time: `${time.getMinutes()}:${time.getSeconds()}`,
time: time,
content: ''
}
let nextId = 0
@@ -263,14 +263,14 @@ const EXAMPLE_MESSAGES = [
}, {
...legacyPaidMessageTemplate,
id: nextId++,
authorName: 'Paryi',
content: 'Welcome Paryi'
authorName: '吾乐KANA',
content: 'Welcome 吾乐KANA!'
}, {
...paidMessageTemplate,
id: nextId++,
authorName: '石油佬',
price: 1245,
content: 'Sent 小电视飞船x1'
authorName: '无火的残渣',
price: 66600,
content: 'Sent 小电视飞船x100'
}, {
...textMessageTemplate,
id: nextId++,
@@ -280,7 +280,7 @@ const EXAMPLE_MESSAGES = [
}, {
...paidMessageTemplate,
id: nextId++,
authorName: '臭DD',
authorName: '夏色祭保護協会会長',
price: 28,
content: 'Sent 礼花x1'
}
@@ -314,6 +314,10 @@ export default {
return stylegen.getStyle(this.form)
}
},
mounted() {
let observer = new MutationObserver(() => this.$refs.example.scrollToBottom())
observer.observe(this.$refs.exampleContainer, {attributes: true})
},
methods: {
getFontSuggestions(query, callback) {
let res = this.FONTS.map(font => {return {value: font}})
@@ -357,8 +361,8 @@ export default {
#example-container {
position: fixed;
top: 30px;
right: 30px;
width: 400px;
left: calc(210px + 40px + (100vw - 210px - 40px) / 2);
width: calc((100vw - 210px - 40px) / 2 - 40px - 30px);
height: calc(100vh - 110px);
background-color: #444;
@@ -380,6 +384,9 @@ export default {
background-position:0 0, 16px 0, 16px -16px, 0px 16px;
padding: 25px;
resize: both;
overflow: hidden;
}
.app-wrapper.mobile #example-container {

View File

@@ -63,6 +63,8 @@ export const DEFAULT_CONFIG = {
reverseSlide: false
}
const FALLBACK_FONTS = ', "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\\5FAE\\8F6F\\96C5\\9ED1", SimHei, Arial, sans-serif'
export function setLocalConfig (config) {
config = mergeConfig(config, DEFAULT_CONFIG)
window.localStorage.stylegenConfig = JSON.stringify(config)
@@ -105,7 +107,7 @@ yt-live-chat-author-chip #author-name {
/* Outlines */
yt-live-chat-renderer * {
${getShowOutlinesStyle(config)}
font-family: "${config.messageFont}", sans-serif;
font-family: "${config.messageFont}"${FALLBACK_FONTS};
font-size: ${config.messageFontSize}px !important;
line-height: ${config.messageLineHeight}px !important;
}
@@ -116,11 +118,11 @@ yt-live-chat-legacy-paid-message-renderer #content {
}
/* Hide scrollbar. */
yt-live-chat-item-list-renderer #items{
yt-live-chat-item-list-renderer #items {
overflow: hidden !important;
}
yt-live-chat-item-list-renderer #item-scroller{
yt-live-chat-item-list-renderer #item-scroller {
overflow: hidden !important;
}
@@ -164,7 +166,7 @@ yt-live-chat-text-message-renderer #chat-badges {
yt-live-chat-text-message-renderer #timestamp {
display: ${config.showTime ? 'inline' : 'none'} !important;
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
font-family: "${config.timeFont}", sans-serif;
font-family: "${config.timeFont}"${FALLBACK_FONTS};
font-size: ${config.timeFontSize}px !important;
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
}
@@ -188,7 +190,7 @@ yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="memb
/* Channel names. */
yt-live-chat-text-message-renderer #author-name {
${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
font-family: "${config.userNameFont}", sans-serif;
font-family: "${config.userNameFont}"${FALLBACK_FONTS};
font-size: ${config.userNameFontSize}px !important;
line-height: ${config.userNameLineHeight || config.userNameFontSize}px !important;
}
@@ -199,7 +201,7 @@ ${getShowColonStyle(config)}
yt-live-chat-text-message-renderer #message,
yt-live-chat-text-message-renderer #message * {
${config.messageColor ? `color: ${config.messageColor} !important;` : ''}
font-family: "${config.messageFont}", sans-serif;
font-family: "${config.messageFont}"${FALLBACK_FONTS};
font-size: ${config.messageFontSize}px !important;
line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
}
@@ -214,7 +216,7 @@ yt-live-chat-paid-message-renderer #author-name *,
yt-live-chat-legacy-paid-message-renderer #event-text,
yt-live-chat-legacy-paid-message-renderer #event-text * {
${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
font-family: "${config.firstLineFont}", sans-serif;
font-family: "${config.firstLineFont}"${FALLBACK_FONTS};
font-size: ${config.firstLineFontSize}px !important;
line-height: ${config.firstLineLineHeight || config.firstLineFontSize}px !important;
}
@@ -224,7 +226,7 @@ yt-live-chat-paid-message-renderer #purchase-amount *,
yt-live-chat-legacy-paid-message-renderer #detail-text,
yt-live-chat-legacy-paid-message-renderer #detail-text * {
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
font-family: "${config.secondLineFont}", sans-serif;
font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
font-size: ${config.secondLineFontSize}px !important;
line-height: ${config.secondLineLineHeight || config.secondLineFontSize}px !important;
}
@@ -232,7 +234,7 @@ yt-live-chat-legacy-paid-message-renderer #detail-text * {
yt-live-chat-paid-message-renderer #content,
yt-live-chat-paid-message-renderer #content * {
${config.scContentColor ? `color: ${config.scContentColor} !important;` : ''}
font-family: "${config.scContentFont}", sans-serif;
font-family: "${config.scContentFont}"${FALLBACK_FONTS};
font-size: ${config.scContentFontSize}px !important;
line-height: ${config.scContentLineHeight || config.scContentFontSize}px !important;
}
@@ -273,7 +275,7 @@ yt-live-chat-ticker-paid-message-item-renderer *,
yt-live-chat-ticker-sponsor-item-renderer,
yt-live-chat-ticker-sponsor-item-renderer * {
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
font-family: "${config.secondLineFont}", sans-serif;
font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
}
yt-live-chat-mode-change-message-renderer,
@@ -382,11 +384,11 @@ function getAnimationStyle (config) {
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
} }`)
curTime += config.fadeInTime
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none;}`)
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
}
if (config.animateOut) {
curTime += config.animateOutWaitTime * 1000
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none;}`)
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
curTime += config.fadeOutTime
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 0;${!config.slide ? ''
: ` transform: translateX(${config.reverseSlide ? -16 : 16}px);`

21
main.py
View File

@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
import argparse
import webbrowser
import asyncio
import logging
import os
import webbrowser
import tornado.ioloop
import tornado.web
import update
import views.chat
import views.config
import views.main
@@ -20,7 +22,7 @@ WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
def main():
parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播聊天层')
parser.add_argument('--host', help='服务器host默认为127.0.0.1', default='127.0.0.1')
parser.add_argument('--port', help='服务器端口,默认为80', type=int, default=80)
parser.add_argument('--port', help='服务器端口,默认为12450', type=int, default=12450)
parser.add_argument('--debug', help='调试模式', action='store_true')
args = parser.parse_args()
@@ -31,6 +33,8 @@ def main():
level=logging.INFO if not args.debug else logging.DEBUG
)
asyncio.ensure_future(update.check_update())
app = tornado.web.Application(
[
(r'/chat', views.chat.ChatHandler),
@@ -45,10 +49,15 @@ def main():
debug=args.debug,
autoreload=False
)
app.listen(args.port, args.host)
logger.info('服务器启动:%s:%d', args.host, args.port)
url = 'http://localhost' if args.port == 80 else f'http://localhost:{args.port}'
webbrowser.open(url)
try:
app.listen(args.port, args.host)
except OSError:
logger.warning('Address is used %s:%d', args.host, args.port)
return
finally:
url = 'http://localhost' if args.port == 80 else f'http://localhost:{args.port}'
webbrowser.open(url)
logger.info('Server started: %s:%d', args.host, args.port)
tornado.ioloop.IOLoop.current().start()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 154 KiB

15
update.py Normal file
View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
import aiohttp
VERSION = 'v1.2.2'
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'])

View File

@@ -19,7 +19,10 @@ class ApiHandler(tornado.web.RequestHandler):
def prepare(self):
if self.request.headers.get('Content-Type', '').startswith('application/json'):
self.json_args = json.loads(self.request.body)
try:
self.json_args = json.loads(self.request.body)
except json.JSONDecodeError:
self.json_args = None
else:
self.json_args = None

View File

@@ -17,10 +17,13 @@ logger = logging.getLogger(__name__)
class Command(enum.IntEnum):
JOIN_ROOM = 0
ADD_TEXT = 1
ADD_GIFT = 2
ADD_MEMBER = 3
HEARTBEAT = 0
JOIN_ROOM = 1
ADD_TEXT = 2
ADD_GIFT = 3
ADD_MEMBER = 4
ADD_SUPER_CHAT = 5
DEL_SUPER_CHAT = 6
DEFAULT_AVATAR_URL = 'https://static.hdslb.com/images/member/noface.gif'
@@ -48,8 +51,8 @@ async def get_avatar_url(user_id):
return DEFAULT_AVATAR_URL
if _last_avatar_failed_time is not None:
if (cur_time - _last_avatar_failed_time).total_seconds() < 5 * 60 + 3:
# 5分钟以内被ban解封大约要15分钟
if (cur_time - _last_avatar_failed_time).total_seconds() < 3 * 60 + 3:
# 3分钟以内被ban解封大约要15分钟
return DEFAULT_AVATAR_URL
else:
_last_avatar_failed_time = None
@@ -59,7 +62,7 @@ async def get_avatar_url(user_id):
async with _http_session.get('https://api.bilibili.com/x/space/acc/info',
params={'mid': user_id}) as r:
if r.status != 200: # 可能会被B站ban
logger.warning('获取头像失败:status=%d %s uid=%d', r.status, r.reason, user_id)
logger.warning('Failed to fetch avatar: status=%d %s uid=%d', r.status, r.reason, user_id)
_last_avatar_failed_time = cur_time
return DEFAULT_AVATAR_URL
data = await r.json()
@@ -95,7 +98,7 @@ asyncio.ensure_future(_fetch_avatar_loop())
class Room(blivedm.BLiveClient):
def __init__(self, room_id):
super().__init__(room_id, session=_http_session)
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
self.clients: List['ChatHandler'] = []
def stop_and_close(self):
@@ -158,75 +161,69 @@ class Room(blivedm.BLiveClient):
'authorName': message.username
})
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
self.send_message(Command.ADD_SUPER_CHAT, {
'avatarUrl': message.face,
'timestamp': message.start_time,
'authorName': message.uname,
'price': message.price,
'content': message.message,
'id': message.id
})
async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
self.send_message(Command.ADD_SUPER_CHAT, {
'ids': message.ids
})
class RoomManager:
def __init__(self):
self._rooms: Dict[int, Room] = {}
def add_client(self, room_id, client: 'ChatHandler'):
if room_id in self._rooms:
room = self._rooms[room_id]
else:
logger.info('创建房间%d', room_id)
room = Room(room_id)
self._rooms[room_id] = room
room.start()
async def add_client(self, room_id, client: 'ChatHandler'):
if room_id not in self._rooms:
if not await self._add_room(room_id):
client.close()
return
room = self._rooms[room_id]
room.clients.append(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.application.settings['debug']:
self.__send_test_message(room)
client.send_test_message()
def del_client(self, room_id, client: 'ChatHandler'):
if room_id not in self._rooms:
return
room = self._rooms[room_id]
room.clients.remove(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if not room.clients:
logger.info('移除房间%d', room_id)
room.stop_and_close()
del self._rooms[room_id]
self._del_room(room_id)
# 测试用
@staticmethod
def __send_test_message(room):
base_data = {
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
'timestamp': time.time(),
'authorName': 'xfgryujk',
}
text_data = {
**base_data,
'authorType': 0,
'content': '我能吞下玻璃而不伤身体',
'privilegeType': 0,
'isGiftDanmaku': False,
'authorLevel': 20,
'isNewbie': False,
'isMobileVerified': True
}
vip_data = base_data
gift_data = {
**base_data,
'giftName': '礼花',
'giftNum': 1,
'totalCoin': 28000
}
room.send_message(Command.ADD_TEXT, text_data)
text_data['authorName'] = '主播'
text_data['authorType'] = 3
text_data['content'] = "I can eat glass, it doesn't hurt me."
room.send_message(Command.ADD_TEXT, text_data)
room.send_message(Command.ADD_MEMBER, vip_data)
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '节奏风暴'
gift_data['totalCoin'] = 100000
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '摩天大楼'
gift_data['totalCoin'] = 450000
room.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '小电视飞船'
gift_data['totalCoin'] = 1245000
room.send_message(Command.ADD_GIFT, gift_data)
async def _add_room(self, room_id):
if room_id in self._rooms:
return True
logger.info('Creating room %d', room_id)
room = Room(room_id)
self._rooms[room_id] = room
if await room.init_room():
room.start()
return True
else:
self._del_room(room_id)
return False
def _del_room(self, room_id):
if room_id not in self._rooms:
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]
room_manager = RoomManager()
@@ -239,21 +236,24 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
self.room_id = None
def open(self):
logger.info('Websocket连接 %s', self.request.remote_ip)
logger.info('Websocket connected %s', self.request.remote_ip)
def on_message(self, message):
if self.room_id is not None:
return
body = json.loads(message)
if body['cmd'] == Command.JOIN_ROOM:
cmd = body['cmd']
if cmd == Command.HEARTBEAT:
pass
elif cmd == Command.JOIN_ROOM:
if self.room_id is not None:
return
self.room_id = int(body['data']['roomId'])
logger.info('客户端%s加入房间%d', self.request.remote_ip, self.room_id)
room_manager.add_client(self.room_id, self)
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
else:
logger.warning('未知的命令: %s data: %s', body['cmd'], body['data'])
logger.warning('Unknown cmd: %s body: %s', cmd, body)
def on_close(self):
logger.info('Websocket断开 %s room: %s', self.request.remote_ip, self.room_id)
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, str(self.room_id))
if self.room_id is not None:
room_manager.del_client(self.room_id, self)
@@ -262,3 +262,54 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
if self.application.settings['debug']:
return True
return super().check_origin(origin)
# 测试用
def send_test_message(self):
base_data = {
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
'timestamp': time.time(),
'authorName': 'xfgryujk',
}
text_data = {
**base_data,
'authorType': 0,
'content': '我能吞下玻璃而不伤身体',
'privilegeType': 0,
'isGiftDanmaku': False,
'authorLevel': 20,
'isNewbie': False,
'isMobileVerified': True
}
member_data = base_data
gift_data = {
**base_data,
'giftName': '摩天大楼',
'giftNum': 1,
'totalCoin': 450000
}
sc_data = {
**base_data,
'price': 30,
'content': 'The quick brown fox jumps over the lazy dog',
'id': 1
}
self.send_message(Command.ADD_TEXT, text_data)
text_data['authorName'] = '主播'
text_data['authorType'] = 3
text_data['content'] = "I can eat glass, it doesn't hurt me."
self.send_message(Command.ADD_TEXT, text_data)
self.send_message(Command.ADD_MEMBER, member_data)
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
sc_data['price'] = 100
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
sc_data['id'] = 2
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
# self.send_message(Command.DEL_SUPER_CHAT, {'ids': [1, 2]})
self.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '小电视飞船'
gift_data['totalCoin'] = 1245000
self.send_message(Command.ADD_GIFT, gift_data)
def send_message(self, cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
self.write_message(body)

View File

@@ -1,30 +1,35 @@
# -*- coding: utf-8 -*-
import json
import uuid
import views.base
from typing import *
configs: Dict[str, dict] = {}
MAX_CONFIG_SIZE = 100 * 1024
ALLOWED_FIELDS = (
'showDanmaku', 'showGift', 'mergeSimilarDanmaku', 'minGiftPrice', 'maxSpeed',
'blockGiftDanmaku', 'blockLevel', 'blockNewbie', 'blockNotMobileVerified',
'blockKeywords', 'blockUsers', 'blockMedalLevel', 'css'
)
configs: Dict[str, dict] = {}
# noinspection PyAbstractClass
class ConfigsHandler(views.base.ApiHandler):
async def post(self):
if not isinstance(self.json_args, dict):
self.set_status(400)
return
config = self.json_args
config_id = str(uuid.uuid4())
config = {
name: self.json_args[name] for name in ALLOWED_FIELDS
}
config['id'] = config_id
config_str = json.dumps(config)
if len(config_str) > MAX_CONFIG_SIZE:
self.set_status(413)
return
configs[config_id] = config
self.write(config_str)
self.set_status(201)
self.write(config)
self.set_header('Content-Type', 'application/json; charset=UTF-8')
if len(configs) > 10000:
for _, key in zip(range(100), configs):
@@ -34,13 +39,23 @@ class ConfigsHandler(views.base.ApiHandler):
# noinspection PyAbstractClass
class ConfigHandler(views.base.ApiHandler):
async def put(self, config_id):
config = configs.get(config_id, None)
if config is None:
if config_id not in configs:
self.set_status(404)
return
for name in ALLOWED_FIELDS:
config[name] = self.json_args[name]
self.write(config)
if not isinstance(self.json_args, dict):
self.set_status(400)
return
config = self.json_args
config['id'] = config_id
config_str = json.dumps(config)
if len(config_str) > MAX_CONFIG_SIZE:
self.set_status(413)
return
configs[config_id] = config
self.write(config_str)
self.set_header('Content-Type', 'application/json; charset=UTF-8')
async def get(self, config_id):
config = configs.get(config_id, None)