更新弹幕、直播的下载,以及弹幕的转换还有部分接口

This commit is contained in:
zlzw 2024-10-25 17:31:14 +08:00
parent ac7f076721
commit 590c54b777
35 changed files with 1197 additions and 151 deletions

View File

@ -21,6 +21,8 @@ import retrofit2.Response;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {

View File

@ -1,6 +1,9 @@
package com.yutou.biliapi.bean.live;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.biliapi.databases.BiliLiveConfigDatabase;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.math.BigInteger;
import java.util.Objects;
@ -11,6 +14,7 @@ public class LiveRoomConfig {
BigInteger roomId;
String anchorName;
boolean isLogin;
String rootPath="live";
LiveDanmuInfo liveInfo;
LiveRoomInfo roomInfo;
@ -37,4 +41,17 @@ public class LiveRoomConfig {
public int hashCode() {
return Objects.hashCode(roomId);
}
public static LiveRoomConfig buildConfig(String roomId){
BiliLiveConfigDatabase database = new BiliLiveConfigDatabase();
LiveConfigDatabaseBean bean = database.getConfig(new BigInteger(roomId));
LiveRoomConfig config = new LiveRoomConfig();
config.setLoginUid(bean.getRecordUid());
config.setRoomId(bean.getRoomId());
config.setAnchorName(bean.getAnchorName());
config.setLogin(StringUtils.hasText(bean.getRecordUid()));
config.setRootPath(bean.getRecordPath());
return config;
}
}

View File

@ -26,8 +26,12 @@ public class LiveConfigDatabaseBean extends AbsDatabasesBean {
private boolean isRecordDanmu;
@JSONField(name = "keyword")
private List<String> keywordList;
@JSONField(name = "recordPath")
private String recordPath="live";
@JSONField(name = "recordUid")
private String recordUid;
@JSONField(name = "recordLiveModel")
private int recordLiveModel;//0 - ffmpeg 1 - java
@JSONField(name = "recordDanmuDate")
private String recordDanmuDate="* * *";// * * * 星期 | 周日是1
@JSONField(name = "recordLiveDate")

View File

@ -2,6 +2,7 @@ package com.yutou.biliapi.bean.live.database;
import com.alibaba.fastjson2.annotation.JSONField;
import com.yutou.biliapi.bean.websocket.live.WSDanmuData;
import com.yutou.bilibili.datas.DanmuData;
import com.yutou.common.databases.AbsDatabasesBean;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -29,13 +30,12 @@ public class LiveDanmuDatabaseBean extends AbsDatabasesBean {
private String uname;
public LiveDanmuDatabaseBean() {
super("danmu",System.currentTimeMillis());
super("danmu", System.currentTimeMillis());
}
public LiveDanmuDatabaseBean(WSDanmuData danmu) {
super("danmu",danmu.getWs_timer());
super("danmu", danmu.getWs_timer());
this.danmu = danmu.getDanmu();
model = danmu.getModel();
fontSize = danmu.getFontSize();
@ -44,4 +44,15 @@ public class LiveDanmuDatabaseBean extends AbsDatabasesBean {
uid = danmu.getUid();
uname = danmu.getUname();
}
public DanmuData createDanmuData() {
DanmuData data = new DanmuData();
data.setId(id);
data.setDanmu(danmu);
data.setModel(model);
data.setFontSize(fontSize);
data.setFontColor(fontColor);
data.setTime(time);
return data;
}
}

View File

@ -0,0 +1,23 @@
package com.yutou.biliapi.bean.live.database;
import com.alibaba.fastjson2.annotation.JSONField;
import com.yutou.common.databases.AbsDatabasesBean;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
public class LiveVideoDatabaseBean extends AbsDatabasesBean {
@JSONField(name = "info")
String roomInfoJson;
@JSONField(name = "start_time")
Date startTime;
@JSONField(name = "path")
String path;
public LiveVideoDatabaseBean() {
super("live_video", System.currentTimeMillis());
}
}

View File

@ -31,4 +31,8 @@ public class LoginCookieDatabaseBean extends AbsDatabasesBean {
public LoginCookieDatabaseBean() {
super("login_cookie", System.currentTimeMillis());
}
public String toCookieString() {
return "SESSDATA=" + sessdta + "; Path=" + path + "; DedeUserID=" + dedeUserID + "; DedeUserID__ckMd5=" + dedeUserIDCkMd5 + "; bili_jct=" + biliJct + "; Expires=" + expires + "; Domain=" + domain + "; sid=" + sid + "; gourl=" + gourl;
}
}

View File

@ -43,7 +43,7 @@ public class BiliBiliLoginDatabase extends SQLiteManager {
return list.getFirst();
}
for (LoginCookieDatabaseBean bean : list) {
if (bean.getSid().equals(userId)) {
if (bean.getDedeUserID().equals(userId)) {
return bean;
}
}
@ -66,7 +66,7 @@ public class BiliBiliLoginDatabase extends SQLiteManager {
@Override
public String getFileName() {
return "bilibili_login.db";
return "old_bilibili_login.db";
}
@Override

View File

@ -2,13 +2,16 @@ package com.yutou.biliapi.databases;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.common.databases.AbsDatabasesBean;
import com.yutou.common.databases.SQLiteManager;
import org.springframework.util.StringUtils;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class BiliLiveConfigDatabase extends SQLiteManager {
String fileName;
@ -44,12 +47,13 @@ public class BiliLiveConfigDatabase extends SQLiteManager {
return null;
}
for (LiveConfigDatabaseBean bean : list) {
if (bean.getRoomId() == roomId) {
if (Objects.equals(bean.getRoomId(), roomId)) {
return bean;
}
}
return null;
}
public boolean deleteConfig(BigInteger roomId) {
LiveConfigDatabaseBean config = getConfig(roomId);
if (config == null) {
@ -57,6 +61,7 @@ public class BiliLiveConfigDatabase extends SQLiteManager {
}
return delete(config);
}
public List<LiveConfigDatabaseBean> getAllConfig() {
List<LiveConfigDatabaseBean> list = get(getDataBean().get(0).getTableName(), LiveConfigDatabaseBean.class);
if (list.isEmpty()) {

View File

@ -4,36 +4,51 @@ import com.alibaba.fastjson2.util.DateUtils;
import com.yutou.biliapi.bean.live.*;
import com.yutou.biliapi.bean.live.database.*;
import com.yutou.biliapi.bean.websocket.live.*;
import com.yutou.bilibili.Tools.DateFormatUtils;
import com.yutou.common.databases.AbsDatabasesBean;
import com.yutou.common.databases.SQLiteManager;
import com.yutou.common.okhttp.HttpDownloadUtils;
import org.apache.poi.ss.usermodel.DataFormat;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import static com.alibaba.fastjson2.util.DateUtils.DateTimeFormatPattern.DATE_FORMAT_10_DASH;
public class BiliLiveDatabase extends SQLiteManager {
LiveInfoDatabaseBean bean;
LiveRoomConfig config;
String fileName;
File rootPath;
public BiliLiveDatabase(LiveRoomConfig roomConfig, String path) {
this.config = roomConfig;
rootPath = new File(path).getParentFile();
fileName =path;
init();
}
public BiliLiveDatabase(LiveRoomConfig roomConfig) {
String time = DateUtils.format(new Date().getTime(), DATE_FORMAT_10_DASH);
rootPath = new File(roomConfig.getAnchorName() + File.separator + time + roomConfig.getRoomInfo().getTitle());
config=roomConfig;
if(!rootPath.exists()){
rootPath = new File(roomConfig.getRootPath() + File.separator + roomConfig.getAnchorName() + File.separator + time + File.separator + roomConfig.getRoomInfo().getTitle());
config = roomConfig;
if (!rootPath.exists()) {
rootPath.mkdirs();
}
fileName=rootPath.getAbsolutePath()+File.separator+"live.db";
fileName = rootPath.getAbsolutePath() + File.separator + "live.db";
init();
}
@Override
public void init() {
super.init();
HttpDownloadUtils.download(config.getRoomInfo().getUserCover(),rootPath.getAbsolutePath(),"poster.jpg");
if(config.getRoomInfo()!=null) {
HttpDownloadUtils.download(new HttpDownloadUtils.Builder().setUrl(config.getRoomInfo().getUserCover())
.setPath(rootPath.getAbsolutePath())
.setFileName("poster.jpg"));
}
}
@Override
@ -49,17 +64,22 @@ public class BiliLiveDatabase extends SQLiteManager {
new LiveGiftDatabaseBean(),
new LiveInteractWordDatabaseBean(),
new LiveSuperChatDatabaseBean(),
new LiveSourceDatabaseBean()
new LiveSourceDatabaseBean(),
new LiveVideoDatabaseBean()
);
}
public void addLiveInfo(LiveRoomInfo info) {
this.bean = new LiveInfoDatabaseBean(info);
@Override
public void close() {
super.close();
}
List<LiveInfoDatabaseBean> infos = get(bean.getTableName(), LiveInfoDatabaseBean.class);
if (infos.isEmpty()) {
createInfo(bean);
}
public void addLiveInfo(LiveVideoDatabaseBean info) {
createInfo(info);
}
public List<LiveVideoDatabaseBean> getLiveInfos() {
return get(new LiveVideoDatabaseBean().getTableName(), LiveVideoDatabaseBean.class);
}
private void addData(WSData bean) {
@ -75,30 +95,32 @@ public class BiliLiveDatabase extends SQLiteManager {
}
public void addSource(WSData bean) {
System.out.println("BiliLiveDatabase.addSource");
add(new LiveSourceDatabaseBean(bean));
addData(bean);
}
private void createInfo(LiveInfoDatabaseBean bean) {
add(bean);
private void createInfo(LiveVideoDatabaseBean bean) {
String format = DateFormatUtils.format(bean.getSql_time());
if (get(bean.getTableName(), " `sql_time` = '" + format + "'", LiveVideoDatabaseBean.class).isEmpty()) {
add(bean);
} else {
update(bean);
}
}
public List<LiveSourceDatabaseBean> getSource(long startTime, long entTime) {
return get(startTime, entTime, LiveSourceDatabaseBean.class);
}
public <T extends AbsDatabasesBean> List<T> get(long startTime, long entTime, Class<T> clazz) {
public <T extends AbsDatabasesBean> List<T> getOfTime(String startTime, String entTime, Class<T> clazz) {
String tableName = null;
StringBuilder sb = new StringBuilder();
String where = null;
if (startTime != -1) {
sb.append(" `sql_time` >= ").append(startTime);
if (startTime != null) {
sb.append(" `sql_time` >= ").append("\"").append(startTime).append("\"");
}
if (entTime != -1) {
if (entTime != null) {
if (!sb.isEmpty()) {
sb.append(" and ");
}
sb.append(" `sql_time` <= ").append(entTime);
sb.append(" `sql_time` <= ").append("\"").append(entTime).append("\"");
}
if (!sb.isEmpty()) {
where = sb.toString();

View File

@ -4,8 +4,8 @@ import com.aayushatharva.brotli4j.Brotli4jLoader;
import com.aayushatharva.brotli4j.decoder.Decoder;
import com.aayushatharva.brotli4j.decoder.DecoderJNI;
import com.aayushatharva.brotli4j.decoder.DirectDecompress;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.util.DateUtils;
import com.yutou.biliapi.api.LiveApi;
import com.yutou.biliapi.bean.live.LiveDanmuInfo;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
@ -26,7 +26,6 @@ import org.java_websocket.handshake.ServerHandshake;
import retrofit2.Response;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
@ -34,15 +33,19 @@ import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.alibaba.fastjson2.util.DateUtils.DateTimeFormatPattern.DATE_FORMAT_10_DASH;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class WebSocketManager {
ThreadPoolExecutor executor;
private static WebSocketManager instance;
Map<LiveRoomConfig, WebSocketClientTh> roomMap;
Map<LiveRoomConfig, DanmuTask> roomMap;
private final List<String> userStopList = new ArrayList<>();//手动停止列表
private WebSocketManager() {
roomMap = new HashMap<>();
executor = new ThreadPoolExecutor(2, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
}
public static WebSocketManager getInstance() {
@ -56,68 +59,107 @@ public class WebSocketManager {
return roomMap.containsKey(roomConfig);
}
public void addRoom(LiveRoomConfig roomConfig) {
public JSONArray getLiveRoomList() {
JSONArray array = new JSONArray();
array.addAll(roomMap.keySet());
return array;
}
public void addRoom(LiveRoomConfig roomConfig, boolean isUser) {
if (!isUser && userStopList.contains(roomConfig.getRoomId().toString())) {
return;
}
if (checkRoom(roomConfig)) {
return;
}
LiveApi api = BiliLiveNetApiManager.getInstance().getApi(roomConfig.getLoginUid());
Response<HttpBody<LiveRoomInfo>> execute = null;
try {
execute = api.getRoomInfo(roomConfig.getRoomId().toString()).execute();
if (execute.isSuccessful()) {
roomConfig.setRoomInfo(execute.body() != null ? execute.body().getData() : null);
}
} catch (IOException e) {
throw new RuntimeException(e);
if (isUser) {
userStopList.remove(roomConfig.getRoomId().toString());
}
api.getLiveRoomDanmuInfo(String.valueOf(roomConfig.getLoginUid())).enqueue(new HttpCallback<LiveDanmuInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveDanmuInfo response, String rawResponse) {
if (!response.getHostList().isEmpty()) {
LiveDanmuInfo.Host host = response.getHostList().get(0);
String url = "wss://" + host.getHost() + ":" + host.getWssPort() + "/sub";
// url="ws://127.0.0.1:8765";
try {
roomConfig.setLiveInfo(response);
new WebSocketClientTh(new URI(url), roomConfig);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onFailure(Throwable throwable) {
throwable.printStackTrace();
}
});
DanmuTask task = new DanmuTask(roomConfig);
roomMap.put(roomConfig, task);
System.out.println("添加websocket任务");
executor.execute(task);
}
public void stopRoom(LiveRoomConfig roomConfig) {
public void stopRoom(String roomId, boolean isUser) {
LiveRoomConfig roomConfig=new LiveRoomConfig();
roomConfig.setRoomId(new BigInteger(roomId));
if (checkRoom(roomConfig)) {
roomMap.get(roomConfig).close();
roomMap.remove(roomConfig);
}
if (isUser) {
userStopList.add(roomConfig.getRoomId().toString());
}
}
private static class DanmuTask implements Runnable {
LiveRoomConfig roomConfig;
WebSocketClientTh client;
public DanmuTask(LiveRoomConfig config) {
this.roomConfig = config;
WebSocketManager.getInstance().roomMap.put(roomConfig, this);
}
@Override
public void run() {
LiveApi api = BiliLiveNetApiManager.getInstance().getApi(roomConfig.getLoginUid());
Response<HttpBody<LiveRoomInfo>> execute = null;
try {
execute = api.getRoomInfo(roomConfig.getRoomId().toString()).execute();
if (execute.isSuccessful()) {
roomConfig.setRoomInfo(execute.body() != null ? execute.body().getData() : null);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
api.getLiveRoomDanmuInfo(String.valueOf(roomConfig.getRoomId())).enqueue(new HttpCallback<LiveDanmuInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveDanmuInfo response, String rawResponse) {
if (!response.getHostList().isEmpty()) {
LiveDanmuInfo.Host host = response.getHostList().get(0);
String url = "wss://" + host.getHost() + ":" + host.getWssPort() + "/sub";
// url="ws://127.0.0.1:8765";
try {
roomConfig.setLiveInfo(response);
client = new WebSocketClientTh(new URI(url), roomConfig);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onFailure(Throwable throwable) {
throwable.printStackTrace();
}
});
}
public void close() {
client.close();
}
}
private static class WebSocketClientTh extends WebSocketClient {
private LiveRoomConfig roomConfig;
private HeartbeatTask heartbeatTask;
BiliLiveDatabase liveDatabase;
private boolean itTmp = true;
public WebSocketClientTh(URI serverUri, LiveRoomConfig roomId) {
super(serverUri);
System.out.println("WebSocketClientTh.WebSocketClientTh : " + serverUri);
this.roomConfig = roomId;
heartbeatTask = new HeartbeatTask();
Brotli4jLoader.ensureAvailability();
liveDatabase = new BiliLiveDatabase(roomConfig);
Brotli4jLoader.ensureAvailability();
heartbeatTask = new HeartbeatTask();
connect();
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
WebSocketManager.getInstance().roomMap.put(roomConfig, this);
heartbeatTask.setSocket(this);
heartbeatTask.sendInitAuthData();
new Timer().schedule(heartbeatTask, 1000, 30000);
@ -131,7 +173,7 @@ public class WebSocketManager {
@Override
public void onMessage(ByteBuffer bytes) {
System.out.println("WebSocketClientTh.onMessage");
System.out.println("WebSocketClientTh.onMessage: " + roomConfig.getAnchorName());
super.onMessage(bytes);
decompress(bytes.array());
}
@ -161,7 +203,7 @@ public class WebSocketManager {
byte[] bytes = new byte[data.length - 16];
WebSocketHeader header = new WebSocketHeader(data);
System.arraycopy(data, header.getHeaderSize(), bytes, 0, data.length - header.getHeaderSize());
System.out.println("数据大小:" + header.getDataSize() + " 协议:" + header.getAgree() + " 头部大小:" + header.getHeaderSize() + " 命令:" + header.getCmdData());
// System.out.println("数据大小:" + header.getDataSize() + " 协议:" + header.getAgree() + " 头部大小:" + header.getHeaderSize() + " 命令:" + header.getCmdData());
switch (header.getAgree()) {
case 0:
case 1:
@ -183,14 +225,14 @@ public class WebSocketManager {
DirectDecompress directDecompress = Decoder.decompress(bytes);
if (directDecompress.getResultStatus() == DecoderJNI.Status.DONE) {
WebSocketBody body = new WebSocketBody(directDecompress.getDecompressedData());
Log.i("3协议:" + useHeader + " 命令数:" + body.getBodyList().size());
// Log.i("协议:" + useHeader + " 命令数:" + body.getBodyList().size());
for (JSONObject json : body.getBodyList()) {
WSData parse = WSData.parse(json);
liveDatabase.addSource(parse);
Log.i("解压:" + parse);
// Log.i("解压:" + parse);
}
System.out.println();
System.out.println();
// System.out.println();
// System.out.println();
} else {
Log.e(new RuntimeException("解压失败"));
}

View File

@ -24,7 +24,7 @@ public class LiveConfigController {
@RequestMapping(value = "set", method = RequestMethod.POST)
@ResponseBody
public ResultData<JSONObject> setConfig(String url, LiveConfigDatabaseBean bean) {
public JSONObject setConfig(String url, LiveConfigDatabaseBean bean) {
LiveConfigDatabaseBean config = configService.addConfig(url, bean);
if (config != null) {
return ResultData.success(config.toJson());
@ -34,7 +34,7 @@ public class LiveConfigController {
@RequestMapping(value = "update", method = RequestMethod.POST)
@ResponseBody
public ResultData<JSONObject> updateConfig(String roomId, LiveConfigDatabaseBean bean) {
public JSONObject updateConfig(String roomId, LiveConfigDatabaseBean bean) {
LiveConfigDatabaseBean config = configService.updateConfig(new BigInteger(roomId), bean);
if (config != null) {
return ResultData.success(config.toJson());
@ -44,7 +44,7 @@ public class LiveConfigController {
@RequestMapping(value = "get", method = RequestMethod.GET)
@ResponseBody
public ResultData<JSONObject> getConfig(String roomId) {
public JSONObject getConfig(String roomId) {
if ("0".equals(roomId) || !StringUtils.hasText(roomId)) {
return ResultData.fail(ReturnCode.RC999);
}
@ -57,7 +57,7 @@ public class LiveConfigController {
@RequestMapping(value = "all", method = RequestMethod.GET)
@ResponseBody
public ResultData<JSONArray> getAllConfig() {
public JSONObject getAllConfig() {
List<LiveConfigDatabaseBean> config = configService.getAllConfig();
if (config != null) {
return ResultData.success(JSONArray.parseArray(JSONArray.toJSONString(config)));
@ -67,7 +67,7 @@ public class LiveConfigController {
@RequestMapping(value = "delete", method = RequestMethod.GET)
@ResponseBody
public ResultData<JSONObject> deleteConfig(BigInteger roomId) {
public JSONObject deleteConfig(BigInteger roomId) {
if (roomId.equals(BigInteger.ZERO)) {
return ResultData.fail(ReturnCode.RC999);
}

View File

@ -0,0 +1,43 @@
package com.yutou.bilibili.Controllers;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
import com.yutou.biliapi.net.WebSocketManager;
import com.yutou.bilibili.datas.ResultData;
import com.yutou.bilibili.datas.ReturnCode;
import com.yutou.bilibili.services.LiveDanmuService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class LiveDanmuController {
@Resource
LiveDanmuService service;
@ResponseBody
@RequestMapping("/live/danmu/list")
public JSONObject getLiveDanmuList() {
return ResultData.success(service.getLiveRoomList());
}
@ResponseBody
@RequestMapping("/live/danmu/stop")
public JSONObject stopLiveDanmu(String roomId) {
service.stop(roomId);
return ResultData.success(ReturnCode.RC100);
}
@ResponseBody
@RequestMapping("/live/danmu/start")
public JSONObject startLiveDanmu(String roomId) {
service.start(roomId);
return ResultData.success(ReturnCode.RC100);
}
@ResponseBody
@RequestMapping("/live/danmu/file/list")
public JSONObject getDanmuList(String roomId) {
return ResultData.success(service.getDanmuFileList(roomId));
}
}

View File

@ -0,0 +1,49 @@
package com.yutou.bilibili.Controllers;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.biliapi.databases.BiliLiveConfigDatabase;
import com.yutou.biliapi.net.WebSocketManager;
import com.yutou.bilibili.datas.ResultData;
import com.yutou.bilibili.datas.ReturnCode;
import com.yutou.bilibili.services.LiveVideoService;
import jakarta.annotation.Resource;
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 LiveVideoController {
@Resource
LiveVideoService videoService;
@RequestMapping("/live/video/list")
@ResponseBody
public JSONObject getLiveVideoList() {
return ResultData.success(videoService.getDownloadTasks());
}
@RequestMapping("/live/video/stop")
@ResponseBody
public JSONObject stopDownload(String roomId) {
videoService.stop(roomId, true);
return ResultData.success(true);
}
@RequestMapping("/live/video/start")
@ResponseBody
public JSONObject startDownload(String roomId) {
BiliLiveConfigDatabase liveConfigDatabase = new BiliLiveConfigDatabase();
List<LiveConfigDatabaseBean> list = liveConfigDatabase.getAllConfig();
for (LiveConfigDatabaseBean bean : list) {
if (bean.getRoomId().toString().equals(roomId)) {
videoService.start(bean, true);
return ResultData.success(true);
}
}
return ResultData.fail(ReturnCode.RC999);
}
}

View File

@ -0,0 +1,46 @@
package com.yutou.bilibili.Controllers;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.bilibili.Tools.FileServerUtils;
import com.yutou.bilibili.Tools.Tools;
import com.yutou.bilibili.datas.ResultData;
import com.yutou.bilibili.datas.VideoFilePath;
import com.yutou.bilibili.services.LiveVideoService;
import jakarta.annotation.Resource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.File;
import java.util.List;
@Controller
public class VideoFileController {
@Resource
LiveVideoService videoService;
@ResponseBody
@RequestMapping("/file/list")
public JSONObject getFileList(String roomId) {
List<VideoFilePath> list;
if (StringUtils.hasText(roomId)) {
list = videoService.getVideoPath(roomId);
} else {
list = videoService.getAllVideoPath();
}
return ResultData.success(list);
}
@RequestMapping("/file/{base64}")
@ResponseBody
public ResponseEntity<FileSystemResource> getFile(@PathVariable String base64) {
File file = FileServerUtils.toFile(base64);
System.out.println(file.getAbsolutePath());
return Tools.getFile(file);
}
}

View File

@ -1,14 +1,19 @@
package com.yutou.bilibili.Tools;
import com.yutou.bilibili.services.SystemService;
import com.yutou.common.utils.Log;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
@Component
public class ApplicationClose implements ApplicationListener<ContextClosedEvent> {
@Resource
SystemService systemConfigService;
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
Log.i("服务结束");
systemConfigService.stop();
}
}

View File

@ -17,7 +17,7 @@ public class AssTools {
private final Date startTime;
private int y = 0;
private List<String> filters = new ArrayList<>();
private String alpha="80";
private String alpha="100";
/**
* 弹幕转换ass
@ -140,7 +140,7 @@ public class AssTools {
y,
x2,
y,
danmuData.getFontColorHex(),
danmuData.getFontColor(),
alpha,
danmuData.getDanmu()
);

View File

@ -0,0 +1,22 @@
package com.yutou.bilibili.Tools;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateFormatUtils {
public static String format(Date date,String format){
return new SimpleDateFormat(format).format(date);
}
public static String format(long time,String format){
return new SimpleDateFormat(format).format(new Date(time));
}
public static String format(long time){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(time));
}
public static String format(Date date){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(date);
}
public static String format(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
}
}

View File

@ -0,0 +1,18 @@
package com.yutou.bilibili.Tools;
import com.yutou.common.utils.Base64Tools;
import java.io.File;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FileServerUtils {
public static String toUrl(String file) {
return "/file/" + URLEncoder.encode(Base64Tools.encode(file), StandardCharsets.UTF_8);
}
public static File toFile(String base64) {
return new File(URLDecoder.decode(Base64Tools.decode(base64), StandardCharsets.UTF_8));
}
}

View File

@ -13,11 +13,11 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.Random;
import java.util.*;
public class Tools {
@ -203,4 +203,24 @@ public class Tools {
public static String getToDayTime() {
return new SimpleDateFormat("yyyy-MM-dd").format(new Date());
}
//扫描文件夹
public static List<File> scanFile(File file){
List<File> list = new ArrayList<>();
FileVisitor<Path> visitor = new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if ("live.db".equals(file.toFile().getName())) {
list.add(file.toFile());
}
return super.visitFile(file, attrs);
}
};
try {
Files.walkFileTree(Paths.get(file.getAbsolutePath()), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, visitor);
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -9,7 +9,7 @@ public class DanmuData {
private int id;
private int model;//13 滚动弹幕 4 底端弹幕 5 顶端弹幕 6 逆向弹幕 7 精准定位 8 高级弹幕
private int fontSize;
private int fontColor;
private String fontColor;
private long time;
private String uCode;
private String danmu;
@ -20,7 +20,4 @@ public class DanmuData {
return new Date(time);
}
public String getFontColorHex() {
return Integer.toHexString(fontColor);
}
}

View File

@ -1,5 +1,6 @@
package com.yutou.bilibili.datas;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
@Data
@ -9,33 +10,33 @@ public class ResultData<T> {
private T data;
private long timestamp ;
public ResultData (){
public ResultData() {
this.timestamp = System.currentTimeMillis();
}
public static <T> ResultData<T> success(T data) {
public static <T> JSONObject success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(ReturnCode.RC100.getCode());
resultData.setMessage(ReturnCode.RC100.getMessage());
resultData.setData(data);
return resultData;
return JSONObject.parseObject(JSONObject.toJSONString(resultData));
}
public static <T> ResultData<T> fail(int code, String message) {
public static <T> JSONObject fail(int code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
return JSONObject.parseObject(JSONObject.toJSONString(resultData));
}
public static <T> ResultData<T> success(ReturnCode code) {
public static JSONObject success(ReturnCode code) {
return fail(code);
}
public static <T> ResultData<T> fail(ReturnCode code) {
public static <T> JSONObject fail(ReturnCode code) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(code.getCode());
resultData.setMessage(code.getMessage());
return resultData;
return JSONObject.parseObject(JSONObject.toJSONString(resultData));
}
}

View File

@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode;
@Data
public class SystemConfigDatabaseBean extends AbsDatabasesBean {
@JSONField(name = "timer_loop")
private long timerLoop = 5000;
private long timerLoop = 30000;
public SystemConfigDatabaseBean() {

View File

@ -0,0 +1,15 @@
package com.yutou.bilibili.datas;
import lombok.Data;
import java.util.List;
@Data
public class VideoFilePath {
private String name;
private String roomId;
private String path;
private String cover;
private boolean isParent;
private List<VideoFilePath> children;
}

View File

@ -3,7 +3,8 @@ package com.yutou.bilibili.interfaces;
import java.io.File;
public abstract class DownloadInterface {
public void onDownloading(double soFarBytes, double totalBytes){};
public void onDownload(File file){};
public void onError(Exception e){};
public void onDownloadStart(){}
public boolean onDownloading(double soFarBytes, double totalBytes){return true;}
public void onDownload(File file){}
public void onError(Exception e){}
}

View File

@ -1,7 +1,83 @@
package com.yutou.bilibili.services;
import com.alibaba.fastjson2.JSONArray;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.biliapi.bean.live.database.LiveDanmuDatabaseBean;
import com.yutou.biliapi.bean.live.database.LiveVideoDatabaseBean;
import com.yutou.biliapi.databases.BiliLiveConfigDatabase;
import com.yutou.biliapi.databases.BiliLiveDatabase;
import com.yutou.biliapi.net.WebSocketManager;
import com.yutou.bilibili.Tools.AssTools;
import com.yutou.bilibili.Tools.DateFormatUtils;
import com.yutou.bilibili.Tools.Tools;
import com.yutou.common.utils.FFmpegUtils;
import org.springframework.stereotype.Service;
import java.io.File;
import java.math.BigInteger;
import java.util.List;
@Service
public class LiveDanmuService {
public void start(String roomId) {
WebSocketManager.getInstance().addRoom(LiveRoomConfig.buildConfig(roomId), true);
}
public void stop(String roomId) {
WebSocketManager.getInstance().stopRoom(roomId, true);
}
public JSONArray getLiveRoomList() {
return WebSocketManager.getInstance().getLiveRoomList();
}
private BiliLiveDatabase getDatabase(String roomId, File file) {
return new BiliLiveDatabase(LiveRoomConfig.buildConfig(roomId), file.getParent() + File.separator + "live.db");
}
public List<File> getDanmuFileList(String roomId) {
BiliLiveConfigDatabase configDatabase=new BiliLiveConfigDatabase();
LiveConfigDatabaseBean bean = configDatabase.getConfig(new BigInteger(roomId));
configDatabase.close();
return Tools.scanFile(new File(bean.getRecordPath() + File.separator + bean.getAnchorName()));
}
public void saveDanmuXML(LiveVideoDatabaseBean videoDatabaseBean, BiliLiveDatabase database) {
File videoFile = new File(videoDatabaseBean.getPath());
long videoTime = FFmpegUtils.getVideoTime(videoFile) + videoDatabaseBean.getStartTime().getTime();
System.out.println("开始时间:" + videoDatabaseBean.getStartTime().getTime());
System.out.println("结束时间:" + videoTime);
String startTime = DateFormatUtils.format(videoDatabaseBean.getStartTime());
String endTime = DateFormatUtils.format(videoTime);
List<LiveDanmuDatabaseBean> danmus = database.getOfTime(startTime, endTime, LiveDanmuDatabaseBean.class);
System.out.println("弹幕数量:" + danmus.size());
AssTools assTools = new AssTools(videoFile.getName().replace(".flv", ""), videoDatabaseBean.getStartTime());
for (LiveDanmuDatabaseBean dm : danmus) {
assTools.addDanmu(dm.createDanmuData());
}
assTools.saveDanmu(videoFile.getAbsolutePath().replace(".flv", ".ass"));
}
public String toTimeString(long videoTime) {
long seconds = videoTime / 1000;
long hours = seconds / 3600;
long remainingSecondsAfterHours = seconds % 3600;
long minutes = remainingSecondsAfterHours / 60;
long finalRemainingSeconds = remainingSecondsAfterHours % 60;
// long finalRemainingMilliseconds = videoTime % 1000;
return String.format("%d小时%d分钟%d秒", hours, minutes, finalRemainingSeconds);
}
public static void main(String[] args) {
LiveDanmuService service = new LiveDanmuService();
List<File> files = service.getDanmuFileList("22047448");
for (File file : files) {
System.out.println(file);
}
}
}

View File

@ -0,0 +1,377 @@
package com.yutou.bilibili.services;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.util.DateUtils;
import com.yutou.biliapi.api.LiveApi;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
import com.yutou.biliapi.bean.live.LiveRoomInfo;
import com.yutou.biliapi.bean.live.LiveRoomPlayInfo;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.biliapi.bean.live.database.LiveVideoDatabaseBean;
import com.yutou.biliapi.bean.login.LoginCookieDatabaseBean;
import com.yutou.biliapi.databases.BiliBiliLoginDatabase;
import com.yutou.biliapi.databases.BiliLiveConfigDatabase;
import com.yutou.biliapi.databases.BiliLiveDatabase;
import com.yutou.biliapi.enums.LiveProtocol;
import com.yutou.biliapi.enums.LiveVideoCodec;
import com.yutou.biliapi.enums.LiveVideoDefinition;
import com.yutou.biliapi.enums.LiveVideoFormat;
import com.yutou.biliapi.net.BiliLiveNetApiManager;
import com.yutou.biliapi.net.WebSocketManager;
import com.yutou.bilibili.Tools.DateFormatUtils;
import com.yutou.bilibili.Tools.FileServerUtils;
import com.yutou.bilibili.Tools.Tools;
import com.yutou.bilibili.datas.VideoFilePath;
import com.yutou.bilibili.interfaces.DownloadInterface;
import com.yutou.common.okhttp.HttpCallback;
import com.yutou.common.okhttp.HttpDownloadUtils;
import com.yutou.common.utils.ConfigTools;
import com.yutou.common.utils.FFmpegUtils;
import okhttp3.Headers;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileFilter;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.alibaba.fastjson2.util.DateUtils.DateTimeFormatPattern.DATE_FORMAT_10_DASH;
@Service
public class LiveVideoService {
ThreadPoolExecutor executor;
private final Map<LiveConfigDatabaseBean, VideoTask> liveVideoMap = new HashMap<>();
private final List<String> userStopList = new ArrayList<>();//手动停止列表
public LiveVideoService() {
System.out.println("初始化下载服务");
executor = new ThreadPoolExecutor(2, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
}
public void start(LiveConfigDatabaseBean bean, boolean isUser) {
if (!isUser && userStopList.contains(bean.getRoomId().toString())) {
return;
}
if (isUser) {
userStopList.remove(bean.getRoomId().toString());
}
if (liveVideoMap.containsKey(bean)) {
return;
}
System.out.println("添加下载任务:" + liveVideoMap.keySet().size());
liveVideoMap.put(bean, null);
BiliLiveNetApiManager.getInstance().getApi(bean.getRecordUid()).getRoomInfo(bean.getRoomId().toString()).enqueue(new HttpCallback<LiveRoomInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomInfo response, String rawResponse) {
if (response.getLiveStatus() == 1) {
VideoTask task = new VideoTask(bean, response);
executor.execute(task);
liveVideoMap.put(bean, task);
} else {
liveVideoMap.remove(bean);
System.out.println("移除下载");
}
}
@Override
public void onFailure(Throwable throwable) {
liveVideoMap.remove(bean);
System.out.println("移除下载");
}
});
}
public void stop(String roomId, boolean isUser) {
if (isUser) {
userStopList.add(roomId);
}
for (Map.Entry<LiveConfigDatabaseBean, VideoTask> entry : liveVideoMap.entrySet()) {
if (entry.getKey().getRoomId().toString().equals(roomId)) {
entry.getValue().isDownload = false;
liveVideoMap.remove(entry.getKey());
System.out.println("移除下载");
break;
}
}
}
public boolean check(String roomId) {
for (LiveConfigDatabaseBean bean : liveVideoMap.keySet()) {
if (bean.getRoomId().toString().equals(roomId)) {
return true;
}
}
return false;
}
public JSONArray getDownloadTasks() {
JSONArray array = new JSONArray();
array.addAll(liveVideoMap.keySet());
return array;
}
public void stopAll() {
for (VideoTask task : liveVideoMap.values()) {
task.isDownload = false;
}
liveVideoMap.clear();
}
private class VideoTask implements Runnable {
LiveConfigDatabaseBean bean;
boolean isDownload = true;
LiveApi api;
String savePath;
LiveConfigDatabaseBean config;
BiliLiveDatabase database;
LiveVideoDatabaseBean videoDatabaseBean;
LiveRoomInfo roomInfo;
public VideoTask(LiveConfigDatabaseBean bean, LiveRoomInfo roomInfo) {
this.bean = bean;
this.roomInfo = roomInfo;
api = BiliLiveNetApiManager.getInstance().getApi(bean.getRecordUid());
}
@Override
public void run() {
if (roomInfo.getLiveStatus() == 1) {
String time = DateUtils.format(new Date().getTime(), DATE_FORMAT_10_DASH);
savePath = new File(bean.getRecordPath() + File.separator + bean.getAnchorName() + File.separator + time + File.separator + roomInfo.getTitle()).getAbsolutePath();
savePath += File.separator + "[" +
DateUtils.format(new Date(),
"yyyy-MM-dd HH-mm-ss") + "]" + roomInfo.getTitle() + ".flv";
record(bean, roomInfo);
} else {
stop();
}
}
private void stop() {
api.getRoomInfo(config.getRoomId().toString()).enqueue(new HttpCallback<LiveRoomInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomInfo response, String rawResponse) {
if (response.getLiveStatus() == 1) {
LiveVideoService.this.start(bean, false);
} else {
LiveVideoService.this.stop(bean.getRoomId().toString(), false);
}
}
@Override
public void onFailure(Throwable throwable) {
}
});
}
private void record(LiveConfigDatabaseBean bean, LiveRoomInfo roomInfo) {
this.config = bean;
isDownload = true;
LiveRoomConfig config = new LiveRoomConfig();
config.setLoginUid(bean.getRecordUid());
config.setRoomId(bean.getRoomId());
config.setAnchorName(bean.getAnchorName());
config.setLogin(StringUtils.hasText(bean.getRecordUid()));
config.setRoomInfo(roomInfo);
config.setRootPath(bean.getRecordPath());
database = new BiliLiveDatabase(config);
api.getLiveRoomPlayInfo(
bean.getRoomId().toString(),
LiveProtocol.getAll(),
LiveVideoFormat.getAll(),
LiveVideoCodec.getAll(),
LiveVideoDefinition.ORIGINAL.getValue()).enqueue(new HttpCallback<LiveRoomPlayInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomPlayInfo response, String rawResponse) {
LiveRoomPlayInfo.Codec codec = response.getPlayurlInfo().getPlayurl().getStream().get(0).getFormat().get(0).getCodec().get(0);
String url = codec.getUrlInfo().get(0).getHost() + codec.getBaseUrl() + codec.getUrlInfo().get(0).getExtra();
if (bean.getRecordLiveModel() == 1) {
javaRecord(url, response);
} else {
ffmpeg(url, response);
}
}
@Override
public void onFailure(Throwable throwable) {
throwable.printStackTrace();
}
});
}
private void javaRecord(String url, LiveRoomPlayInfo playInfo) {
HttpDownloadUtils.download(new HttpDownloadUtils.Builder()
.setUrl(url)
.setPath(savePath)
.setDownloadInterface(new DownloadInterface() {
@Override
public void onDownloadStart() {
super.onDownloadStart();
VideoTask.this.onStart();
}
@Override
public boolean onDownloading(double soFarBytes, double totalBytes) {
return isDownload;
}
@Override
public void onDownload(File file) {
super.onDownload(file);
stop();
}
@Override
public void onError(Exception e) {
super.onError(e);
stop();
}
}));
}
private void ffmpeg(String url, LiveRoomPlayInfo playInfo) {
String ffmpegPath = ConfigTools.load(ConfigTools.CONFIG, "ffmpeg", String.class);
String cookie = "";
LoginCookieDatabaseBean ck = BiliBiliLoginDatabase.getInstance().getCookie(config.getRecordUid());
if (ck != null) {
cookie = ck.toCookieString();
}
FFmpegUtils command = new FFmpegUtils.Builder()
.withParam("-user_agent", ConfigTools.getUserAgent())
.withParam("-cookies", cookie)
.withParam("-headers", "Referer: https://live.bilibili.com")
// .withNotSymbolParam("-progress", "-")
.withNotSymbolParam("-threads", "8")
.withNotSymbolParam("-c:v", "copy")
.withNotSymbolParam("-y", "")
.build(ffmpegPath, url, savePath);
System.out.println(command);
try {
command.start(new DownloadInterface() {
@Override
public void onDownloadStart() {
super.onDownloadStart();
VideoTask.this.onStart();
}
@Override
public boolean onDownloading(double soFarBytes, double totalBytes) {
if (!isDownload) {
command.stop();
}
return super.onDownloading(soFarBytes, totalBytes);
}
@Override
public void onDownload(File file) {
super.onDownload(file);
System.err.println("下载完成 ");
stop();
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void onStart() {
videoDatabaseBean = new LiveVideoDatabaseBean();
videoDatabaseBean.setPath(savePath);
videoDatabaseBean.setRoomInfoJson(JSONObject.toJSONString(roomInfo));
videoDatabaseBean.setStartTime(new Date());
database.addLiveInfo(videoDatabaseBean);
}
}
public List<VideoFilePath> getAllVideoPath() {
BiliLiveConfigDatabase configDatabase = new BiliLiveConfigDatabase();
List<LiveConfigDatabaseBean> list = configDatabase.getAllConfig();
configDatabase.close();
List<VideoFilePath> filePathList = new ArrayList<>();
for (LiveConfigDatabaseBean bean : list) {
filePathList.addAll(getVideoFilePath(bean));
}
return filePathList;
}
public List<VideoFilePath> getVideoPath(String roomId) {
BiliLiveConfigDatabase configDatabase = new BiliLiveConfigDatabase();
LiveConfigDatabaseBean bean = configDatabase.getConfig(new BigInteger(roomId));
configDatabase.close();
return new ArrayList<>(getVideoFilePath(bean));
}
private List<VideoFilePath> getVideoFilePath(LiveConfigDatabaseBean configBean) {
List<VideoFilePath> filePathList = new ArrayList<>();
String recordPath = configBean.getRecordPath() + File.separator + configBean.getAnchorName();
File recordDir = new File(recordPath);
if (recordDir.exists()) {
List<File> files = Tools.scanFile(recordDir).stream()
.filter(file -> "live.db".equals(file.getName()))
.toList();
if (!files.isEmpty()) {
for (File db : files) {
VideoFilePath path = createVideoRootFilePath(configBean, db);
BiliLiveDatabase database = new BiliLiveDatabase(LiveRoomConfig.buildConfig(configBean.getRoomId().toString()), db.getAbsolutePath());
List<LiveVideoDatabaseBean> infos = database.getLiveInfos();
database.close();
path.setChildren(getVideoInfo(infos));
filePathList.add(path);
}
}
}
return filePathList;
}
private VideoFilePath createVideoRootFilePath(LiveConfigDatabaseBean config, File db) {
VideoFilePath path = new VideoFilePath();
path.setRoomId(config.getRoomId().toString());
path.setCover(config.getAnchorFace());
path.setName(config.getAnchorName());
path.setParent(true);
path.setPath(FileServerUtils.toUrl(db.getParent()));
return path;
}
private List<VideoFilePath> getVideoInfo(List<LiveVideoDatabaseBean> videoList) {
List<VideoFilePath> filePathList = new ArrayList<>();
for (LiveVideoDatabaseBean bean : videoList) {
VideoFilePath path = new VideoFilePath();
LiveRoomInfo roomInfo = JSONObject.parseObject(bean.getRoomInfoJson(), LiveRoomInfo.class);
path.setRoomId(roomInfo.getRoomId().toString());
path.setName(roomInfo.getTitle());
path.setPath(DateFormatUtils.format(bean.getSql_time()));
path.setCover(FileServerUtils.toUrl(new File(bean.getPath()).getParent() + File.separator + "poster.jpg"));
path.setParent(false);
path.setChildren(null);
filePathList.add(path);
}
return filePathList;
}
public static void main(String[] args) {
LiveVideoService service = new LiveVideoService();
List<VideoFilePath> path = service.getAllVideoPath();
System.out.println("path.size() = " + path.size());
System.out.println(JSONArray.toJSONString(path));
}
}

View File

@ -27,6 +27,8 @@ import java.util.concurrent.TimeUnit;
@Service
public class SystemService {
@Resource
LiveVideoService videoService;
SystemConfigDatabases databases = new SystemConfigDatabases();
private ScheduledExecutorService timer;
private ScheduledFuture<?> scheduled;
@ -50,6 +52,7 @@ public class SystemService {
}
scheduled = timer.scheduleAtFixedRate(() -> {
List<LiveConfigDatabaseBean> list = liveConfigDatabase.getAllConfig();
System.out.println("循环任务:"+list.size());
for (LiveConfigDatabaseBean bean : list) {
if (bean.isRecordDanmu() && bean.checkRecordDanmuTime()) {
recordDanmu(bean);
@ -60,7 +63,10 @@ public class SystemService {
}
}, 0, getLoopTimer(), TimeUnit.MILLISECONDS);
}
public void stop() {
scheduled.cancel(true);
videoService.stopAll();
}
// 录制弹幕
private void recordDanmu(LiveConfigDatabaseBean bean) {
LiveRoomConfig config = new LiveRoomConfig();
@ -68,12 +74,13 @@ public class SystemService {
config.setRoomId(bean.getRoomId());
config.setAnchorName(bean.getAnchorName());
config.setLogin(StringUtils.hasText(bean.getRecordUid()));
WebSocketManager.getInstance().addRoom(config);
config.setRootPath(bean.getRecordPath());
WebSocketManager.getInstance().addRoom(config,false);
}
// 录制视频
private void recordVideo(LiveConfigDatabaseBean bean) {
videoService.start(bean, false);
}
public static void main(String[] args) throws InterruptedException {
@ -85,4 +92,5 @@ public class SystemService {
service.start();
}
}

View File

@ -56,7 +56,7 @@ public class BiliBiliLiveDatabasesManager extends SQLiteManager {
"values (%d,%d,%d,%d,'%s','%s',%d,'%s')"
,data.getModel()
,data.getFontSize()
,data.getFontColor()
// ,data.getFontColor()
,data.getTime()
,data.getUCode()
,data.getDanmu()
@ -87,7 +87,7 @@ public class BiliBiliLiveDatabasesManager extends SQLiteManager {
data.setId(set.getInt("id"));
data.setModel(set.getInt("model"));
data.setFontSize(set.getInt("fontSize"));
data.setFontColor(set.getInt("fontColor"));
// data.setFontColor(set.getInt("fontColor"));
data.setTime(set.getLong("time"));
data.setUCode(set.getString("uCode"));
data.setDanmu(set.getString("danmu"));

View File

@ -40,7 +40,7 @@ public class AbsDatabasesBean {
json.put(key,"");
}
}
System.out.println("创建" + tableName + "表 json:" + json);
//System.out.println("创建" + tableName + "表 json:" + json);
return json;
}

View File

@ -10,6 +10,7 @@ import com.yutou.common.inter.ISqlDatabaseBean;
import com.yutou.common.utils.Log;
import lombok.Data;
import lombok.Getter;
import org.sqlite.SQLiteConfig;
import java.io.File;
import java.lang.reflect.Field;
@ -36,7 +37,9 @@ public abstract class SQLiteManager {
items = new ArrayList<>();
for (Field field : fields) {
String name = field.getAnnotation(JSONField.class).name();
if (name.equals("tableName")) continue;
if ("tableName".equals(name)) {
continue;
}
String type = BuildSqlItem.TYPE_STRING;
if (field.getType() == int.class) {
type = BuildSqlItem.TYPE_INT;
@ -97,10 +100,10 @@ public abstract class SQLiteManager {
}
}
public <T extends AbsDatabasesBean> void add(T t) {
protected <T extends AbsDatabasesBean> void add(T t) {
StringBuilder sb = new StringBuilder();
try {
Statement statement = conn.createStatement();
StringBuilder sb = new StringBuilder();
StringBuilder value = new StringBuilder();
sb.append("INSERT INTO `").append(t.getTableName()).append("` ");
sb.append("(");
@ -112,22 +115,44 @@ public abstract class SQLiteManager {
continue;
}
sb.append("`").append(key).append("`,");
value.append("'").append(json.get(key)).append("',");
//value.append("'").append(json.get(key)).append("',");
value.append("?").append(",");
}
sb.deleteCharAt(sb.length() - 1);
value.deleteCharAt(value.length() - 1);
value.append(")");
sb.append(") VALUES ");
sb.append(value);
statement.executeUpdate(sb.toString());
PreparedStatement statement = conn.prepareStatement(sb.toString());
int i = 1;
for (String key : keySet) {
if ("id".equals(key)) {
continue;
}
if (json.get(key) instanceof String) {
statement.setString(i++, json.get(key).toString());
} else if (json.get(key) instanceof Integer) {
statement.setInt(i++, json.getInteger(key));
} else if (json.get(key) instanceof Long) {
statement.setLong(i++, json.getLong(key));
} else if (json.get(key) instanceof BigInteger) {
statement.setObject(i++, json.get(key));
} else if (json.get(key) instanceof Boolean) {
statement.setBoolean(i++, json.getBoolean(key));
} else {
statement.setObject(i++, json.get(key));
}
}
statement.execute();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
System.err.println(sb);
throw new RuntimeException(e);
}
}
public <T extends AbsDatabasesBean> void update(T t) {
protected <T extends AbsDatabasesBean> void update(T t) {
try {
String id = DateUtils.format(t.getSql_time(), "yyyy-MM-dd HH:mm:ss.SSS");
Statement statement = conn.createStatement();
@ -158,11 +183,11 @@ public abstract class SQLiteManager {
}
public <T extends AbsDatabasesBean> List<T> get(String table, Class<T> tClass) {
protected <T extends AbsDatabasesBean> List<T> get(String table, Class<T> tClass) {
return get(table, null, tClass);
}
public <T extends AbsDatabasesBean> List<T> get(String table, String where, Class<T> tClass) {
protected <T extends AbsDatabasesBean> List<T> get(String table, String where, Class<T> tClass) {
List<T> list = new ArrayList<>();
try {
Statement statement = conn.createStatement();
@ -177,9 +202,16 @@ public abstract class SQLiteManager {
String name = resultSet.getMetaData().getColumnName(i + 1);
Object value = resultSet.getObject(name);
if (value instanceof String && ((String) value).startsWith("[")) {
json.put(name, JSON.parseArray((String) value));
try {
json.put(name, JSON.parseArray((String) value));
} catch (Exception e) {
json.put(name, value);
}
} else {
json.put(name, resultSet.getObject(name));
json.put(name, value);
if (json.get(name) == null || "null".equals(json.get(name).toString())) {
json.put(name, null);
}
}
}
list.add(json.to(tClass));
@ -214,7 +246,7 @@ public abstract class SQLiteManager {
try {
sql.mkdirs();
sql.delete();
conn = DriverManager.getConnection(url + sql.getAbsolutePath());
linkDB();
} catch (Exception e) {
Log.e(e);
}
@ -273,28 +305,17 @@ public abstract class SQLiteManager {
if (!sql.exists()) {
createSql(buildSql);
} else {
conn = DriverManager.getConnection(url + sql.getAbsolutePath());
linkDB();
}
} catch (Exception e) {
Log.e(e);
}
}
public boolean setDB(String fileName) {
try {
Class.forName("org.sqlite.JDBC");
sql = new File("db" + File.separator + fileName);
if (sql.exists()) {
if (conn != null && !conn.isClosed()) {
conn.close();
}
conn = DriverManager.getConnection(url + sql.getAbsolutePath());
return true;
}
} catch (Exception e) {
Log.e(e);
}
return false;
private void linkDB() throws SQLException {
SQLiteConfig config = new SQLiteConfig();
config.enforceForeignKeys(true);
conn = config.createConnection(url + sql.getAbsolutePath());
}

View File

@ -18,10 +18,11 @@ public abstract class FileCallback<T> implements Callback<FileBody<T>> {
private static ThreadPoolExecutor executor;
private final T bean;
private String savePath;
public FileCallback(T bean) {
public FileCallback(T bean, String savePath) {
this.bean = bean;
this.savePath = savePath;
if (executor == null) {
executor = new ThreadPoolExecutor(2, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
}
@ -45,7 +46,7 @@ public abstract class FileCallback<T> implements Callback<FileBody<T>> {
public void run() {
try {
System.out.println("开始下载");
File file = new File("download" + File.separator + System.currentTimeMillis() + ".flv");
File file = new File(savePath);
onStart(bean);
if (!file.exists()) {
boolean mkdirs = file.getParentFile().mkdirs();
@ -67,6 +68,7 @@ public abstract class FileCallback<T> implements Callback<FileBody<T>> {
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
onFailure(bean,e);
} finally {
onFinish(bean);
try {
@ -91,7 +93,13 @@ public abstract class FileCallback<T> implements Callback<FileBody<T>> {
@Override
public void onResponse(Call<FileBody<T>> call, Response<FileBody<T>> response) {
executor.execute(new DownloadTask(bean, response.headers(), call.request().url(), response.body().getInputStream()));
try {
executor.execute(new DownloadTask(bean, response.headers(), call.request().url(), response.body().getInputStream()));
} catch (Exception e) {
e.printStackTrace();
onFailure(bean,e);
call.cancel();
}
}
@Override

View File

@ -1,48 +1,115 @@
package com.yutou.common.okhttp;
import com.yutou.bilibili.interfaces.DownloadInterface;
import com.yutou.common.utils.ConfigTools;
import com.yutou.common.utils.Log;
import lombok.Data;
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class HttpDownloadUtils {
public static void download(String url, String path,String fileName) {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.get()
.url(url)
public static void download(Builder builder) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES)
.build();
Request.Builder rb = new Request.Builder()
.get()
.addHeader("User-Agent", ConfigTools.getUserAgent())
.url(builder.url);
if (StringUtils.hasText(builder.cookie)) {
rb.addHeader("Set-Cookie", builder.cookie);
}
Request request = rb.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
if (builder.downloadInterface != null) {
builder.downloadInterface.onError(e);
}
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
InputStream inputStream = Objects.requireNonNull(response.body()).byteStream();
File target = new File(path, fileName);
File target;
if (StringUtils.hasText(builder.fileName)) {
target = new File(builder.path, builder.fileName);
} else {
target = new File(builder.path);
}
FileOutputStream fileOutputStream = new FileOutputStream(target);
if (builder.downloadInterface != null) {
builder.downloadInterface.onDownloadStart();
}
try {
byte[] buffer = new byte[2048];
int len;
long soFarBytes = 0;
long totalBytes = inputStream.available();
while ((len = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
soFarBytes += len;
if (builder.downloadInterface != null) {
if (!builder.downloadInterface.onDownloading(soFarBytes, totalBytes)) {
break;
}
}
}
fileOutputStream.flush();
} catch (IOException e) {
Log.getLogger("download").log(Level.FINE, "download error:"+url, e);
Log.getLogger("download").log(Level.FINE, "download error:" + builder.url, e);
} finally {
if (builder.downloadInterface != null) {
builder.downloadInterface.onDownload(target);
}
}
}
});
}
@Data
public static class Builder {
String url;
String path;
String fileName;
DownloadInterface downloadInterface;
String cookie;
public Builder setUrl(String url) {
this.url = url;
return this;
}
public Builder setPath(String path) {
this.path = path;
return this;
}
public Builder setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Builder setDownloadInterface(DownloadInterface downloadInterface) {
this.downloadInterface = downloadInterface;
return this;
}
public Builder setCookie(String cookie) {
this.cookie = cookie;
return this;
}
}
}

View File

@ -1,5 +1,6 @@
package com.yutou.common.okhttp;
import com.yutou.common.utils.ConfigTools;
import okhttp3.Request;
import java.util.HashMap;
@ -38,7 +39,7 @@ public class ParamsContext {
break;
}
headerMap.remove("tableName");
headerMap.put("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36");
headerMap.put("User-Agent", ConfigTools.getUserAgent());
return iRequestParam.getRequest(headerMap, map, request);
}
}

View File

@ -2,6 +2,7 @@ package com.yutou.common.utils;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.util.StringUtils;
import java.io.*;
@ -93,4 +94,16 @@ public class ConfigTools {
}
return null;
}
public static String getUserAgent() {
String ua=load(CONFIG,"userAgent",String.class);
if(!StringUtils.hasText(ua)){
ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36";
}
return ua;
}
public static boolean saveUserAgent(String ua) {
return save(CONFIG,"userAgent",ua);
}
}

View File

@ -0,0 +1,128 @@
package com.yutou.common.utils;
import com.yutou.bilibili.interfaces.DownloadInterface;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
public class FFmpegUtils {
private String command;
InputStream inputStream;
OutputStream outputStream;
private FFmpegUtils() {
}
public static class Builder {
private final Map<String, String> params = new LinkedHashMap<>();
private final Map<String, String> paramsNotSymbol = new LinkedHashMap<>();
public Builder withParam(String key, String value) {
if (value != null) {
params.put(key, value);
}
return this;
}
public Builder withNotSymbolParam(String key, String value) {
if (value != null) {
paramsNotSymbol.put(key, value);
}
return this;
}
private String getCommand(String ffmpegPath, String inputUrl, String outputPath) {
StringBuilder command = new StringBuilder(ffmpegPath);
for (Map.Entry<String, String> entry : params.entrySet()) {
command.append(" ").append(entry.getKey());
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
command.append(" ").append("\"").append(entry.getValue()).append("\"");
}
}
command.append(" -i ").append("\"").append(inputUrl).append("\"");
for (Map.Entry<String, String> entry : paramsNotSymbol.entrySet()) {
command.append(" ").append(entry.getKey());
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
command.append(" ").append(entry.getValue());
}
}
command.append(" ").append("\"").append(outputPath).append("\"");
return command.toString();
}
public FFmpegUtils build(String ffmpegPath, String inputUrl, String outputPath) {
FFmpegUtils ffmpeg = new FFmpegUtils();
ffmpeg.command = getCommand(ffmpegPath, inputUrl, outputPath);
return ffmpeg;
}
}
public void start(DownloadInterface downloadInterface) {
new Thread(() -> {
try {
Process process = AppTools.exec(command);
inputStream = process.getErrorStream();
outputStream = process.getOutputStream();
byte[] bytes = new byte[2048];
if (downloadInterface != null) {
downloadInterface.onDownloadStart();
}
while (inputStream.read(bytes) > -1) {
if (downloadInterface != null) {
downloadInterface.onDownloading(0, 0);
}
}
//获取视频时长ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4
//获取到结果:5372.432000
if (downloadInterface != null) {
System.out.println("触发下载完成");
downloadInterface.onDownload(null);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}).start();
}
public void stop() {
try {
outputStream.write("q".getBytes());
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 获取视频时长
* @param video 视频文件
* @return 毫秒
*/
public static long getVideoTime(File video) {
String ffprobe = ConfigTools.load(ConfigTools.CONFIG, "ffprobe", String.class);
String exec = ffprobe + " -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"" + video.getAbsolutePath() + "\"";
try {
System.out.println(exec);
Process process = AppTools.exec(exec);
InputStream inputStream = process.getInputStream();
byte[] bytes = new byte[2048];
String data;
inputStream.read(bytes);
data = new String(bytes);
inputStream.close();
process.destroy();
process.exitValue();
return (long) (Double.parseDouble(data)*1000);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}