新增了关键词检测

新增了手动暂停的恢复
This commit is contained in:
zlzw 2024-11-29 15:45:29 +08:00
parent 31dd9cf1a1
commit e4e5696b70
11 changed files with 298 additions and 33 deletions

View File

@ -43,6 +43,7 @@
layer.open({
type: 2,
title: "添加新房间",
maxmin: true,
area: ['600px', '500px'],
content: '/html/ui/createConfig.html?roomId=' + roomId
@ -52,6 +53,7 @@
layer.open({
type: 2,
title: "批量编辑",
maxmin: true,
area: ['600px', '500px'],
content: '/html/ui/createConfig.html?array=' + array
@ -88,7 +90,7 @@
{ field: 'anchorName', title: '用户名', width: 100, fixed: 'left' },
{ field: 'anchorFace', title: '头像', width: 80, templet: '<div><image src="" onerror="showImage(\'{{= d.anchorFace }}\',this);" style="width: 30px;height: 30px;"></div>' },
{ field: 'live_room_id', title: '房间号', width: 80, templet: '<div><a href="https://live.bilibili.com/{{= d.live_room_id}}" target="_blank">{{= d.live_room_id}}</a>' },
{ field: 'recordPath', title: '保存路径', width: 120 },
{ field: 'keyword', title: '监听关键词', width: 120 },
{ field: 'recordDanmu', title: '录制弹幕', width: 120, sort: true },
{ field: 'recordLive', title: '录制视频', width: 120, sort: true },
{ field: 'syncDanmuForLive', title: '同步录制', width: 120, sort: true },

View File

@ -1,5 +1,10 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/layui/css/layui.css">
<link rel="stylesheet" href="/css/inputTag.css">
<style>
.layui-form-label {
width: 120px !important;
@ -33,7 +38,7 @@
<label class="layui-form-label">录制视频时<br>同步录制弹幕</label>
<div class="layui-input-block"><br>
<input type="checkbox" name="syncDanmuForLive" lay-skin="switch" lay-filter="switchSync" title="启用|禁用">
<i class="layui-icon layui-icon-help layui-text-em " onclick="timeTips2(this)"></i>
<i class="layui-icon layui-icon-help layui-text-em " onclick="timeTips(3,this)"></i>
<br>
<br>
</div>
@ -51,13 +56,21 @@
<input type="checkbox" name="week_7" title="周日" lay-skin="tag">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">关键词检测</label>
<div class="layui-input-inline fairy-tag-container" style="width: 60%;">
<input type="text" id="keywordList" name="keywordList" autocomplete="off"
class="layui-input fairy-tag-input ">
</div>
<i class="layui-icon layui-icon-help" onclick="timeTips(4,this)"></i>
</div>
<div class="layui-form-item">
<label class="layui-form-label">弹幕录制时间</label>
<div class="layui-input-inline">
<input type="text" name="recordDanmuDate" id="recordDanmuDate" value="00:00:00 - 23:59:59"
lay-verify="required" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-text-em" onclick="timeTips(false,this)"><i
<div class="layui-form-mid layui-text-em" onclick="timeTips(1,this)"><i
class="layui-icon layui-icon-help"></i> </div>
</div>
<div class="layui-form-item">
@ -66,7 +79,7 @@
<input type="text" name="recordLiveDate" id="recordLiveDate" value="00:00:00 - 23:59:59"
lay-verify="required" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-text-em" onclick="timeTips(true,this)"><i
<div class="layui-form-mid layui-text-em" onclick="timeTips(2,this)"><i
class="layui-icon layui-icon-help"></i> </div>
</div>
<div class="layui-inline">
@ -98,19 +111,20 @@
<script src="/js/jquery-3.2.1.js"></script>
<script src="/js/httpUtils.js"></script>
<script src="/js/CommonConfig.js"></script>
<script src="/js/inputTag.js"></script>
<script>
function timeTips(isLive, that) {
if (isLive) {
layer.tips('是从开始到结束时间范围内,主播开播会启动录制,超过结束范围不会中断正在录制的任务', that);
} else {
layer.tips('仅在当前时间范围内录制,如遇到正在直播,则延迟到下播时停止录制', that);
function timeTips(type, that) {
var tips = ''
switch (type) {
case 1: tips = '仅在时间范围内录制,如遇到正在直播,则延迟到下播时停止录制<p style="color:red;">注:如果启用同步录制功能,那本设置将无效</p>'; break;
case 2: tips = '仅在时间范围内录制,主播开播会启动录制,正在直播时超过结束范围则延迟到下播时停止录制'; break;
case 3: tips = '启用后,录制视频时会同步录制弹幕,下播后会同步停止录制.同时上面录制弹幕按钮将失效<p style="color:red;">启用该选项必须启用录制视频功能才可有效</p>'; break;
case 4: tips = '在监测时间范围内(周X、时段),开播标题包含关键词才会开始录制<p style="color:red;">仅针对录制视频功能,录制弹幕不受关键词影响</p>'; break;
}
layer.alert(tips)
}
function timeTips2(that) {
layer.tips('启用后,录制视频时会同步录制弹幕,下播后会同步停止录制.同时上面录制弹幕按钮将失效', that);
}
var roomId = getParam("roomId");
var editArray = getParam("array")
@ -119,7 +133,16 @@
var layer = layui.layer;
var laytpl = layui.laytpl;
var laydate = layui.laydate;
var inputTag = layui.inputTag;
var windowsIndex;
var keywordList=[];
inputTag = inputTag.render({
elem: '#keywordList',
data: [],
onChange: function (data, value, type) {
keywordList = data;
}
});
form.render();
// 提交事件
form.on('submit(submit-form)', function (data) {
@ -136,6 +159,7 @@
weeks.push(i.toString());
}
}
field.keywordList = keywordList
field.weeks = weeks;
console.log(field)
if (editArray === null) {
@ -230,6 +254,11 @@
for (let i = 0; i < json.weeks.length; i++) {
result[`week_${json.weeks[i]}`] = true;
}
if(json.keyword===undefined||json.keyword===null){
json.keyword=[]
}
keywordList = json.keyword;
inputTag.setData(json.keyword)
form.val('form-filter', result);
if (json.syncDanmuForLive) {
$("[name='recordDanmu']").prop("disabled", true);

View File

@ -68,15 +68,15 @@
<p>开播时长:{{= item.liveTime}}</p>
直播录制状态:
{{# if(item.downloadVideo){ }}
<span style="color: #16b777" onclick="clickVideo('{{= item.roomId}}',true,this)">录制中</span>
<span style="color: #16b777" onclick="clickVideo('{{= item.roomId}}',true,this)">{{= item.videoListen}}</span>
{{# } else{ }}
<span style="color: #FD482C" onclick="clickVideo('{{= item.roomId}}',false,this)">待机中</span>
<span style="color: #FD482C" onclick="clickVideo('{{= item.roomId}}',false,this)">{{= item.videoListen}}</span>
{{# }; }}<br>
弹幕录制状态:
{{# if(item.danmu){ }}
<span style="color: #16b777" onclick="clickDanmu('{{= item.roomId}}',true,this)">录制中</span>
<span style="color: #16b777" onclick="clickDanmu('{{= item.roomId}}',true,this)">{{= item.danmuListen}}</span>
{{# } else{ }}
<span style="color: #FD482C" onclick="clickDanmu('{{= item.roomId}}',false,this)">待机中</span>
<span style="color: #FD482C" onclick="clickDanmu('{{= item.roomId}}',false,this)">{{= item.danmuListen}}</span>
{{# }; }}<br>
</div>
</div>
@ -98,8 +98,8 @@
console.log(roomId)
window.open("https://live.bilibili.com/" + roomId, '_blank')
}
function clickVideo(roomId, status,that) {
const title = "是否" + (status ? "停止" : "启动") + "录制视频?"
function clickVideo(roomId, status, that) {
const title = "是否" + (status ? "停止" : "启动") + "录制视频?<p style='color:red;'>手动干预后不再自动监听,第二天或重新配置可清除该状态</p>"
layer.confirm(title, { icon: 3 }, function () {
if (status) {
stopLiveVideo(roomId)
@ -122,13 +122,13 @@
});
}
function clickDanmu(roomId, status) {
const title = "是否" + (status ? "停止" : "启动") + "录制弹幕?"
const title = "是否" + (status ? "停止" : "启动") + "录制弹幕?<p style='color:red;'>手动干预后不再自动监听,第二天或重新配置可清除该状态</p>"
layer.confirm(title, { icon: 3 }, function () {
if (status) {
stopLiveDanmu(roomId)
.then(data => {
layer.msg(data.message, { icon: (data.status == 100 ? 1 : 0) }, function () {
// location.reload();
// location.reload();
});
})
@ -136,12 +136,12 @@
startLiveDanmu(roomId)
.then(data => {
layer.msg(data.message, { icon: (data.status == 100 ? 1 : 0) }, function () {
// location.reload();
// location.reload();
});
})
}
}, function () {
});
}
function confirmFollow(userId) {
@ -198,7 +198,7 @@
// if(document.getElementById(item.data)!==null){
// document.getElementById(item.data).innerHTML=item.message;
// }
// })
initFollowStatus(true)

170
Web/js/inputTag.js Normal file
View File

@ -0,0 +1,170 @@
/*
* Name: inputTag
* Author: cshaptx4869
* Project: https://github.com/cshaptx4869/inputTag
*/
(function (define) {
define(['jquery'], function ($) {
"use strict";
class InputTag {
options = {
elem: '.fairy-tag-input',
theme: ['fairy-bg-red', 'fairy-bg-orange', 'fairy-bg-green', 'fairy-bg-cyan', 'fairy-bg-blue', 'fairy-bg-black'],
data: [],
removeKeyNum: 8,
createKeyNum: 13,
permanentData: [],
sortable: false
};
get elem() {
return $(this.options.elem);
}
get copyData() {
return [...this.options.data];
}
constructor(options) {
this.render(options);
}
render(options) {
this.init(options);
this.listen();
}
setData(data){
this.options.data=data;
this.init(this.options)
}
init(options) {
var spans = '', that = this;
this.options = $.extend(this.options, options);
!this.elem.attr('placeholder') && this.elem.attr('placeholder', '添加标签');
$.each(this.options.data, function (index, item) {
spans += that.spanHtml(item);
});
this.elem.before(spans);
}
listen() {
var that = this;
this.elem.parent().on('click', 'a', function () {
that.removeItem($(this).parent('span'));
});
this.elem.parent().on('click', function () {
that.elem.focus();
});
this.elem.keydown(function (event) {
var keyNum = (event.keyCode ? event.keyCode : event.which);
if (keyNum === that.options.removeKeyNum) {
if (!that.elem.val().trim()) {
var closeItems = that.elem.parent().find('a');
if (closeItems.length) {
that.removeItem($(closeItems[closeItems.length - 1]).parent('span'));
event.preventDefault();
}
}
} else if (keyNum === that.options.createKeyNum) {
that.createItem();
event.preventDefault();
}
});
this.elem.blur(function () {
that.createItem();
});
if (this.options.sortable) {
Sortable.create(this.elem.parent()[0], {
handle: 'span',
onSort: function (event) {
that.options.data = that.elem.parent()
.find('span.fairy-tag>span')
.map(function (index, item) {
return $(item).text();
}).toArray();
that.onChange($(event.item).children('span').text(), 'sort');
}
});
}
}
createItem() {
var value = this.elem.val().trim();
if (this.options.beforeCreate && typeof this.options.beforeCreate === 'function') {
var modifiedValue = this.options.beforeCreate(this.copyData, value);
if (typeof modifiedValue == 'string' && modifiedValue) {
value = modifiedValue;
} else {
value = '';
}
}
if (value) {
if (!this.options.data.includes(value)) {
this.options.data.push(value);
this.elem.before(this.spanHtml(value));
this.onChange(value, 'create');
}
}
this.elem.val('');
}
removeItem(target) {
var that = this;
var closeSpan = target.remove(),
closeSpanText = $(closeSpan).children('span').text();
var value = that.options.data.splice($.inArray(closeSpanText, that.options.data), 1);
value.length === 1 && that.onChange(value[0], 'remove');
}
randomColor() {
return this.options.theme[Math.floor(Math.random() * this.options.theme.length)];
}
spanHtml(value) {
return '<span class="fairy-tag fairy-anim-fadein ' + this.randomColor() + '">' +
'<span>' + value + '</span>' +
(this.options.permanentData.includes(value) ? '' : '<a href="#" title="删除标签">&times;</a>') +
'</span>';
}
onChange(value, type) {
this.options.onChange && typeof this.options.onChange === 'function' && this.options.onChange(this.copyData, value, type);
}
getData() {
return this.copyData;
}
clearData() {
this.options.data = [];
this.elem.prevAll('span.fairy-tag').remove();
}
}
return {
render(options) {
return new InputTag(options);
}
}
});
}(typeof define === 'function' && define.amd ? define : function (deps, factory) {
var MOD_NAME = 'inputTag';
if (typeof module !== 'undefined' && module.exports) { //Node
module.exports = factory(require('jquery'));
} else if (window.layui && layui.define) {
layui.define('jquery', function (exports) { //layui加载
exports(MOD_NAME, factory(layui.jquery));
});
} else {
window[MOD_NAME] = factory(window.jQuery);
}
}));

View File

@ -37,7 +37,7 @@ public class LiveConfigDatabaseBean extends AbsDatabasesBean {
@JSONField(name = "syncDanmuForLive")
private boolean isSyncDanmuForLive;
@JSONField(name = "keyword")
private List<String> keywordList;
private List<String> keywordList=new ArrayList<>();
@JSONField(name = "weeks")
private List<String> weeks=Arrays.asList("1","2","3","4","5","6","7");
@JSONField(name = "recordPath")

View File

@ -97,6 +97,14 @@ public class WebSocketServer {
}
}
public void removeUserStopList(String roomId) {
userStopList.remove(roomId);
}
public boolean isUserStopList(String roomId) {
return userStopList.contains(roomId);
}
private class DanmuTask implements Runnable {
LiveRoomConfig roomConfig;
WebSocketClientTh client;

View File

@ -139,7 +139,7 @@ public class LiveConfigController {
@RequestMapping(value = "set/array", method = RequestMethod.POST)
@ResponseBody
public JSONObject addArrayConfig(@RequestBody JSONObject jsonObject) {
JSONObject config=jsonObject.getJSONObject("config");
JSONObject config = jsonObject.getJSONObject("config");
LiveConfigDatabaseBean bean = config.to(LiveConfigDatabaseBean.class);
if (!bean.verifyLiveTimer()) {
return ResultData.fail(ReturnCode.RC999.getCode(), "视频录制时间格式错误");
@ -147,15 +147,18 @@ public class LiveConfigController {
if (!bean.verifyDanmuTimer()) {
return ResultData.fail(ReturnCode.RC999.getCode(), "弹幕录制时间格式错误");
}
if("on".equals(config.getString("recordDanmu"))){
if ("on".equals(config.getString("recordDanmu"))) {
bean.setRecordDanmu(true);
}
if("on".equals(config.getString("recordLive"))){
if ("on".equals(config.getString("recordLive"))) {
bean.setRecordLive(true);
}
if("on".equals(config.getString("syncDanmuForLive"))){
if ("on".equals(config.getString("syncDanmuForLive"))) {
bean.setSyncDanmuForLive(true);
}
if (config.containsKey("keywordList")) {
bean.setKeywordList(config.getList("keywordList", String.class));
}
JSONArray jsonArray = jsonObject.getJSONArray("array");
List<LiveConfigDatabaseBean> list = jsonArray.stream().map(roomId -> configService.addConfig(roomId.toString(), bean)).toList();
int countNull = list.stream().filter(Objects::isNull).toList().size();

View File

@ -1,5 +1,6 @@
package com.yutou.bilibili.Controllers;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.bilibili.datas.ResultData;
import com.yutou.bilibili.services.LiveDanmuService;
@ -10,6 +11,8 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
public class LiveController {
@Resource
@ -22,13 +25,31 @@ public class LiveController {
@RequestMapping("/live/list")
@ResponseBody
public JSONObject getLiveList(int page,int limit) {
return ResultData.success(liveService.getLiveList(page,limit), liveService.getConfigCount());
public JSONObject getLiveList(int page, int limit) {
List<JSONObject> list = liveService.getLiveList(page, limit).stream().map(it -> {
JSONObject json = JSONObject.parseObject(JSON.toJSONString(it));
if (videoService.isUserStopList(it.getRoomId())) {
json.put("videoListen", "已暂停");
} else if (it.isDownloadVideo()) {
json.put("videoListen", "录制中");
} else {
json.put("videoListen", "待机中");
}
if (danmuService.isUserStopList(it.getRoomId())) {
json.put("danmuListen", "已暂停");
} else if (it.isDanmu()) {
json.put("danmuListen", "录制中");
} else {
json.put("danmuListen", "待机中");
}
return json;
}).toList();
return ResultData.success(list, liveService.getConfigCount());
}
@RequestMapping("/live/gift/info")
@ResponseBody
public JSONObject download(String roomId, String videoId) {
return ResultData.success(liveService.getGiftInfo(roomId,videoId));
return ResultData.success(liveService.getGiftInfo(roomId, videoId));
}
}

View File

@ -19,6 +19,10 @@ import java.util.List;
public class LiveConfigService {
@Resource
LiveDatabasesService databasesService;
@Resource
LiveDanmuService danmuService;
@Resource
LiveVideoDownloadService videoDownloadService;
public LiveConfigDatabaseBean addConfig(String url, LiveConfigDatabaseBean bean) {
if (!StringUtils.hasText(url)) {
@ -37,6 +41,8 @@ public class LiveConfigService {
bean.setAnchorFace(infoBean.getInfo().getFace());
bean.setAnchorName(infoBean.getInfo().getUname());
databasesService.getConfigDatabase().setConfig(bean);
danmuService.removeUserStopList(roomId);
videoDownloadService.removeUserStopList(roomId);
return bean;
} catch (IOException e) {
throw new RuntimeException(e);
@ -66,7 +72,8 @@ public class LiveConfigService {
public List<LiveConfigDatabaseBean> getAllConfig() {
return databasesService.getConfigDatabase().getAllConfig();
}
public List<LiveConfigDatabaseBean> getConfigs(int page,int limit) {
public List<LiveConfigDatabaseBean> getConfigs(int page, int limit) {
return databasesService.getConfigDatabase().getConfigs(page, limit);
}

View File

@ -54,6 +54,12 @@ public class LiveDanmuService {
public void clearUserList() {
webSocketServer.clearUserStopList();
}
public void removeUserStopList(String roomId) {
webSocketServer.removeUserStopList(roomId);
}
public boolean isUserStopList(String roomId) {
return webSocketServer.isUserStopList(roomId);
}
public List<File> getDanmuFileList(String roomId) {
LiveConfigDatabaseBean bean = liveDatabasesService.getConfigDatabase().getConfig(roomId);

View File

@ -67,6 +67,12 @@ public class LiveVideoDownloadService {
public void clearUserStopList() {
userStopList.clear();
}
public void removeUserStopList(String roomId) {
userStopList.remove(roomId);
}
public boolean isUserStopList(String roomId) {
return userStopList.contains(roomId);
}
public void start(LiveConfigDatabaseBean bean, boolean isUser) {
if (!isUser && userStopList.contains(bean.getRoomId())) {
@ -82,6 +88,19 @@ public class LiveVideoDownloadService {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomInfo response, String rawResponse) {
if (response.getLiveStatus() == 1) {
if (!bean.getKeywordList().isEmpty() && !isUser) {
String foundKey = bean.getKeywordList().stream()
.filter(key -> response.getTitle().contains(key))
.findFirst()
.orElse(null);
if (foundKey != null) {
Log.i(response.getRoomId(), "检测到开播关键词", foundKey, response.getTitle());
} else {
Log.i(response.getRoomId(), "未检测到关键词", response.getTitle(), Arrays.toString(bean.getKeywordList().toArray()));
return;
}
}
VideoTask task = new VideoTask(bean, response);
executor.execute(task);
} else {
@ -281,7 +300,7 @@ public class LiveVideoDownloadService {
// .withNotSymbolParam("-threads", "8")//看bili-go也没有加这个改成设置好了
// .withNotSymbolParam("-bufsize", "10M")
.withNotSymbolParam("-f", "segment")
.withNotSymbolParam("-rw_timeout","60000000")
.withNotSymbolParam("-rw_timeout", "60000000")
.withNotSymbolParam("-segment_time", "60")
.withNotSymbolParam("-segment_format", "mpegts")
.withNotSymbolParam("-map", "0")