diff --git a/.dockerignore b/.dockerignore index 7549a713..ab6ea41b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,10 +7,15 @@ build/ **/node_modules/ # IDEs and editors -/.idea +.idea/ # misc **/.git* *.spec screenshots/ README.md + +# runtime data +data/* +!data/config.ini +log/* diff --git a/Dockerfile b/Dockerfile index 58889d71..82bc1a22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,20 +14,22 @@ RUN wget https://nodejs.org/dist/v10.16.0/node-v10.16.0-linux-x64.tar.xz \ && ln -s /node-v10.16.0-linux-x64/bin/npm /usr/local/bin/npm # 后端依赖 -COPY requirements.txt /blivechat/ -RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r /blivechat/requirements.txt +WORKDIR /blivechat +COPY requirements.txt ./ +RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 前端依赖 -WORKDIR /blivechat/frontend -COPY frontend/package*.json ./ +WORKDIR ./frontend +COPY frontend/package.json frontend/package-lock.json ./ RUN npm i --registry=https://registry.npm.taobao.org -# 编译 -COPY . /blivechat +# 编译前端 +COPY . ../ RUN npm run build # 运行 -WORKDIR /blivechat +WORKDIR .. +VOLUME /blivechat/data /blivechat/log /blivechat/frontend/dist EXPOSE 12450 ENTRYPOINT ["python3", "main.py"] CMD ["--host", "0.0.0.0", "--port", "12450"] diff --git a/README.md b/README.md index 3d12a0f2..7c34d926 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ * 支持自动翻译弹幕、醒目留言到日语 ## 使用方法 -### 本地使用 +### 一、本地使用 1. 下载[发布版](https://github.com/xfgryujk/blivechat/releases)(仅提供x64 Windows版) 2. 双击`blivechat.exe`运行服务器,或者用命令行可以指定host和端口号: ```bat @@ -28,18 +28,25 @@ **注意事项:** -* 应该先启动blivechat后启动OBS,否则网页会加载失败,这时应该刷新OBS的浏览器源,显示Loaded则加载成功 * 本地使用时不要关闭blivechat.exe那个黑框,否则不能继续获取弹幕 * 样式生成器没有列出所有本地字体,但是可以手动输入本地字体 -### 公共服务器 -请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能出现直播事故 +### 二、公共服务器 +请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能发生直播事故 * [第三方公共服务器](http://chat.bilisc.com/) * [仅样式生成器](https://style.vtbs.moe/) -### 源代码版 -1. 编译前端(需要安装Node.js和npm): +### 三、源代码版(自建服务器或在Windows以外平台) +0. 由于使用了git子模块,clone时需要加上`--recursive`参数: + ```sh + git clone --recursive https://github.com/xfgryujk/blivechat.git + ``` + 如果已经clone,拉子模块的方法: + ```sh + git submodule update --init --recursive + ``` +1. 编译前端(需要安装Node.js): ```sh cd frontend npm i @@ -56,8 +63,73 @@ ``` 3. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略 -### Docker +### 四、Docker(自建服务器) 1. ```sh - docker run -d -p 12450:12450 xfgryujk/blivechat:latest + docker run --name blivechat -d -p 12450:12450 \ + --mount source=blc-data,target=/blivechat/data \ + --mount source=blc-log,target=/blivechat/log \ + --mount source=blc-frontend,target=/blivechat/frontend/dist \ + xfgryujk/blivechat:latest ``` 2. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略 + +### nginx配置(可选) +自建服务器时使用,`sudo vim /etc/nginx/sites-enabled/blivechat.conf` + +```conf +upstream blivechat { + keepalive 8; + # blivechat地址 + server 127.0.0.1:12450; +} + +# 强制HTTPS +server { + listen 80; + listen [::]:80; + server_name YOUR.DOMAIN.NAME; + + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name YOUR.DOMAIN.NAME; + + # SSL + ssl_certificate /PATH/TO/CERT.crt; + ssl_certificate_key /PATH/TO/CERT_KEY.key; + + # 代理header + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # 静态文件 + location / { + root /PATH/TO/BLIVECHAT/frontend/dist; + # 如果文件不存在,交给前端路由 + try_files $uri $uri/ /index.html; + } + # 动态API + location /api { + proxy_pass http://blivechat; + } + # websocket + location = /api/chat { + proxy_pass http://blivechat; + + # 代理websocket必须设置 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + # 由于这个块有proxy_set_header,这些不会自动继承 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` diff --git a/api/chat.py b/api/chat.py index a87ceb7d..9ea1ad77 100644 --- a/api/chat.py +++ b/api/chat.py @@ -71,7 +71,7 @@ def __parse_gift(self, command): def __parse_buy_guard(self, command): data = command['data'] return self._on_buy_guard(blivedm.GuardBuyMessage( - data['uid'], data['username'], None, None, None, + data['uid'], data['username'], data['guard_level'], None, None, None, None, data['start_time'], None )) @@ -149,34 +149,21 @@ async def __on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage): id_ = uuid.uuid4().hex # 为了节省带宽用list而不是dict - self.send_message(Command.ADD_TEXT, [ - # 0: avatarUrl + self.send_message(Command.ADD_TEXT, make_text_message( await models.avatar.get_avatar_url(danmaku.uid), - # 1: timestamp int(danmaku.timestamp / 1000), - # 2: authorName danmaku.uname, - # 3: authorType author_type, - # 4: content danmaku.msg, - # 5: privilegeType danmaku.privilege_type, - # 6: isGiftDanmaku - 1 if danmaku.msg_type else 0, - # 7: authorLevel + danmaku.msg_type, danmaku.user_level, - # 8: isNewbie - 1 if danmaku.urank < 10000 else 0, - # 9: isMobileVerified - 1 if danmaku.mobile_verify else 0, - # 10: medalLevel + danmaku.urank < 10000, + danmaku.mobile_verify, 0 if danmaku.room_id != self.room_id else danmaku.medal_level, - # 11: id id_, - # 12: translation translation - ]) + )) if need_translate: await self._translate_and_response(danmaku.msg, id_) @@ -206,7 +193,8 @@ async def __on_buy_guard(self, message: blivedm.GuardBuyMessage): 'id': id_, 'avatarUrl': await models.avatar.get_avatar_url(message.uid), 'timestamp': message.start_time, - 'authorName': message.username + 'authorName': message.username, + 'privilegeType': message.guard_level }) async def _on_super_chat(self, message: blivedm.SuperChatMessage): @@ -244,8 +232,10 @@ async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage): }) def _need_translate(self, text): + cfg = config.get_config() return ( - config.get_config().enable_translate + cfg.enable_translate + and (not cfg.allow_translate_rooms or self.room_id in cfg.allow_translate_rooms) and self.auto_translate_count > 0 and models.translate.need_translate(text) ) @@ -266,6 +256,39 @@ async def _translate_and_response(self, text, msg_id): ) +def make_text_message(avatar_url, timestamp, author_name, author_type, content, privilege_type, + is_gift_danmaku, author_level, is_newbie, is_mobile_verified, medal_level, + id_, translation): + return [ + # 0: avatarUrl + avatar_url, + # 1: timestamp + timestamp, + # 2: authorName + author_name, + # 3: authorType + author_type, + # 4: content + content, + # 5: privilegeType + privilege_type, + # 6: isGiftDanmaku + 1 if is_gift_danmaku else 0, + # 7: authorLevel + author_level, + # 8: isNewbie + 1 if is_newbie else 0, + # 9: isMobileVerified + 1 if is_mobile_verified else 0, + # 10: medalLevel + medal_level, + # 11: id + id_, + # 12: translation + translation + ] + + class RoomManager: def __init__(self): self._rooms: Dict[int, Room] = {} @@ -284,8 +307,7 @@ async def add_client(self, room_id, client: 'ChatHandler'): if client.auto_translate: room.auto_translate_count += 1 - if client.application.settings['debug']: - await client.send_test_message() + await client.on_join_room() def del_client(self, room_id, client: 'ChatHandler'): room = self._rooms.get(room_id, None) @@ -390,6 +412,41 @@ def check_origin(self, origin): return True return super().check_origin(origin) + @property + def has_joined_room(self): + return self.room_id is not None + + def send_message(self, cmd, data): + body = json.dumps({'cmd': cmd, 'data': data}) + try: + self.write_message(body) + except tornado.websocket.WebSocketClosedError: + self.on_close() + + async def on_join_room(self): + if self.application.settings['debug']: + await self.send_test_message() + + # 不允许自动翻译的提示 + if self.auto_translate: + cfg = config.get_config() + if cfg.allow_translate_rooms and self.room_id not in cfg.allow_translate_rooms: + self.send_message(Command.ADD_TEXT, make_text_message( + models.avatar.DEFAULT_AVATAR_URL, + int(time.time()), + 'blivechat', + 2, + 'Translation is not allowed in this room. Please download to use translation', + 0, + False, + 60, + False, + True, + 0, + uuid.uuid4().hex, + '' + )) + # 测试用 async def send_test_message(self): base_data = { @@ -397,37 +454,25 @@ async def send_test_message(self): 'timestamp': int(time.time()), 'authorName': 'xfgryujk', } - text_data = [ - # 0: avatarUrl + text_data = make_text_message( base_data['avatarUrl'], - # 1: timestamp base_data['timestamp'], - # 2: authorName base_data['authorName'], - # 3: authorType 0, - # 4: content '我能吞下玻璃而不伤身体', - # 5: privilegeType 0, - # 6: isGiftDanmaku - 0, - # 7: authorLevel + False, 20, - # 8: isNewbie - 0, - # 9: isMobileVerified - 1, - # 10: medalLevel + False, + True, 0, - # 11: id uuid.uuid4().hex, - # 12: translation '' - ] + ) member_data = { **base_data, - 'id': uuid.uuid4().hex + 'id': uuid.uuid4().hex, + 'privilegeType': 3 } gift_data = { **base_data, @@ -461,14 +506,3 @@ async def send_test_message(self): gift_data['totalCoin'] = 1245000 gift_data['giftName'] = '小电视飞船' self.send_message(Command.ADD_GIFT, gift_data) - - @property - def has_joined_room(self): - return self.room_id is not None - - def send_message(self, cmd, data): - body = json.dumps({'cmd': cmd, 'data': data}) - try: - self.write_message(body) - except tornado.websocket.WebSocketClosedError: - self.on_close() diff --git a/api/main.py b/api/main.py index 1846619f..a24ef521 100644 --- a/api/main.py +++ b/api/main.py @@ -8,9 +8,15 @@ class MainHandler(tornado.web.StaticFileHandler): - """为了使用Vue Router的history模式,把所有请求转发到index.html""" - async def get(self, *args, **kwargs): - await super().get('index.html', *args, **kwargs) + """为了使用Vue Router的history模式,把不存在的文件请求转发到index.html""" + async def get(self, path, include_body=True): + try: + await super().get(path, include_body) + except tornado.web.HTTPError as e: + if e.status_code != 404: + raise + # 不存在的文件请求转发到index.html,交给前端路由 + await super().get('index.html', include_body) # noinspection PyAbstractClass @@ -20,6 +26,7 @@ async def get(self): self.write({ 'version': update.VERSION, 'config': { - 'enableTranslate': cfg.enable_translate + 'enableTranslate': cfg.enable_translate, + 'loaderUrl': cfg.loader_url } }) diff --git a/blivedm b/blivedm index d173228c..4c64c1bd 160000 --- a/blivedm +++ b/blivedm @@ -1 +1 @@ -Subproject commit d173228c5f83c2f5f94551259e0e6c01e929d92c +Subproject commit 4c64c1bd1e9fe634894d7b781eab1fef0e753907 diff --git a/config.py b/config.py index 47f348fd..ec3dc925 100644 --- a/config.py +++ b/config.py @@ -13,14 +13,20 @@ def init(): - reload() + if reload(): + return + logger.warning('Using default config') + global _config + _config = AppConfig() def reload(): config = AppConfig() - if config.load(CONFIG_PATH): - global _config - _config = config + if not config.load(CONFIG_PATH): + return False + global _config + _config = config + return True def get_config(): @@ -31,14 +37,29 @@ class AppConfig: def __init__(self): self.database_url = 'sqlite:///data/database.db' self.enable_translate = True + self.allow_translate_rooms = {} + self.tornado_xheaders = False + self.loader_url = '' def load(self, path): - config = configparser.ConfigParser() - config.read(path) try: + config = configparser.ConfigParser() + config.read(path) + app_section = config['app'] self.database_url = app_section['database_url'] self.enable_translate = app_section.getboolean('enable_translate') + + allow_translate_rooms = app_section['allow_translate_rooms'] + if allow_translate_rooms == '': + self.allow_translate_rooms = {} + else: + allow_translate_rooms = allow_translate_rooms.split(',') + self.allow_translate_rooms = set(map(lambda id_: int(id_.strip()), allow_translate_rooms)) + + self.tornado_xheaders = app_section.getboolean('tornado_xheaders') + self.loader_url = app_section['loader_url'] + except (KeyError, ValueError): logger.exception('Failed to load config:') return False diff --git a/data/config.ini b/data/config.ini index 8e6edd67..b0df8cea 100644 --- a/data/config.ini +++ b/data/config.ini @@ -1,11 +1,25 @@ [app] # 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 +# 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 = + +# Set to true if you are using a reverse proxy server such as nginx +tornado_xheaders = false + +# 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 = diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js index 96c1c7e8..a4a44a2b 100644 --- a/frontend/src/api/config.js +++ b/frontend/src/api/config.js @@ -5,7 +5,7 @@ export const DEFAULT_CONFIG = { showDanmaku: true, showGift: true, showGiftName: false, - mergeSimilarDanmaku: true, + mergeSimilarDanmaku: false, mergeGift: true, maxNumber: 60, diff --git a/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue b/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue deleted file mode 100644 index ebd7cc59..00000000 --- a/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - diff --git a/frontend/src/components/ChatRenderer/MembershipItem.vue b/frontend/src/components/ChatRenderer/MembershipItem.vue new file mode 100644 index 00000000..3bd5525e --- /dev/null +++ b/frontend/src/components/ChatRenderer/MembershipItem.vue @@ -0,0 +1,421 @@ + + + + + + diff --git a/frontend/src/components/ChatRenderer/PaidMessage.vue b/frontend/src/components/ChatRenderer/PaidMessage.vue index 5c87d2e2..4809d7d2 100644 --- a/frontend/src/components/ChatRenderer/PaidMessage.vue +++ b/frontend/src/components/ChatRenderer/PaidMessage.vue @@ -56,7 +56,7 @@ export default { return 'CN¥' + utils.formatCurrency(this.price) }, timeText() { - return utils.getTimeTextMinSec(this.time) + return utils.getTimeTextHourMin(this.time) } } } diff --git a/frontend/src/components/ChatRenderer/TextMessage.vue b/frontend/src/components/ChatRenderer/TextMessage.vue index c3c3a101..6d0ebd7c 100644 --- a/frontend/src/components/ChatRenderer/TextMessage.vue +++ b/frontend/src/components/ChatRenderer/TextMessage.vue @@ -54,7 +54,7 @@ export default { }, computed: { timeText() { - return utils.getTimeTextMinSec(this.time) + return utils.getTimeTextHourMin(this.time) }, authorTypeText() { return constants.AUTHOR_TYPE_TO_TEXT[this.authorType] diff --git a/frontend/src/components/ChatRenderer/Ticker.vue b/frontend/src/components/ChatRenderer/Ticker.vue index 28abd494..0f8e4599 100644 --- a/frontend/src/components/ChatRenderer/Ticker.vue +++ b/frontend/src/components/ChatRenderer/Ticker.vue @@ -26,11 +26,11 @@