新增B站视频下载功能 #7

Merged
root merged 2 commits from dev_ into master 2022-08-15 02:39:57 +08:00
4 changed files with 227 additions and 37 deletions
Showing only changes of commit e19d1cdeda - Show all commits

View File

@ -0,0 +1,29 @@
package com.yutou.qqbot.Controllers;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.qqbot.models.BiliBili.BiliVideo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class BiliBiliController {
@RequestMapping("/bilibili/down.do")
@ResponseBody
public JSONObject downloadBili(String data) {
new Thread(() -> {
JSONObject json = JSONObject.parseObject(data);
System.out.println("json = " + json);
String url = json.getString("url");
boolean downDanmu = json.containsKey("danmu") && "on".equals(json.getString("danmu"));
boolean merge = json.containsKey("merge") && "on".equals(json.getString("merge"));
BiliVideo video = new BiliVideo();
video.downVideo(url, downDanmu, merge);
}
).start();
JSONObject json = new JSONObject();
json.put("msg", "ok");
json.put("code", 0);
return json;
}
}

View File

@ -1,16 +1,9 @@
package com.yutou.qqbot.models.BiliBili; package com.yutou.qqbot.models.BiliBili;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.TextFormat;
import com.google.protobuf.UnknownFieldSet;
import com.google.protobuf.util.JsonFormat;
import com.yutou.qqbot.Annotations.UseModel; import com.yutou.qqbot.Annotations.UseModel;
import com.yutou.qqbot.bilibili.*; import com.yutou.qqbot.bilibili.*;
import com.yutou.qqbot.interfaces.DownloadInterface;
import com.yutou.qqbot.interfaces.ObjectInterface; import com.yutou.qqbot.interfaces.ObjectInterface;
import com.yutou.qqbot.models.Model; import com.yutou.qqbot.models.Model;
import com.yutou.qqbot.utlis.AppTools; import com.yutou.qqbot.utlis.AppTools;
@ -18,22 +11,18 @@ import com.yutou.qqbot.utlis.ConfigTools;
import com.yutou.qqbot.utlis.HttpTools; import com.yutou.qqbot.utlis.HttpTools;
import com.yutou.qqbot.utlis.StringUtils; import com.yutou.qqbot.utlis.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.Inflater;
@UseModel @UseModel
public class BiliVideo extends Model { public class BiliVideo extends Model {
public String downloadPath = "tmp"; public String downloadPath = "tmp";
List<DanmuData> danmuDatas = new ArrayList<>();
long danmuNextTime = 0;
@Override @Override
public boolean isUserPublic() { public boolean isUserPublic() {
@ -50,7 +39,11 @@ public class BiliVideo extends Model {
return "B站视频下载"; return "B站视频下载";
} }
private void downVideo(String url) { public void downVideo(String url) {
downVideo(url, true, false);
}
public void downVideo(String url, boolean downDanmu, boolean merge) {
if (!new BiliLogin().testLogin()) { if (!new BiliLogin().testLogin()) {
System.err.println("未登录"); System.err.println("未登录");
return; return;
@ -58,6 +51,7 @@ public class BiliVideo extends Model {
if (!url.contains("?")) { if (!url.contains("?")) {
url += "?"; url += "?";
} }
danmuDatas.clear();
JSONObject info = getVideoInfo(url); JSONObject info = getVideoInfo(url);
if (info.getInteger("code") == 0) { if (info.getInteger("code") == 0) {
JSONObject infoData = info.getJSONObject("data"); JSONObject infoData = info.getJSONObject("data");
@ -96,6 +90,8 @@ public class BiliVideo extends Model {
eps.put("cid", infoData.getLong("cid")); eps.put("cid", infoData.getLong("cid"));
} }
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("danmu", downDanmu);
json.put("merge", merge);
json.put("avid", infoData.getLong("aid")); json.put("avid", infoData.getLong("aid"));
if (eps.containsKey("cid")) { if (eps.containsKey("cid")) {
json.put("cid", eps.getLong("cid")); json.put("cid", eps.getLong("cid"));
@ -104,6 +100,11 @@ public class BiliVideo extends Model {
json.put("fourk", 1); json.put("fourk", 1);
downVideo(json, eps); downVideo(json, eps);
} else { } else {
System.out.println("json = " + json);
System.out.println("eps = " + eps);
List<File> list = new ArrayList<>();
String root = new File("tmp").getAbsolutePath() + File.separator;
for (Object o : eps.getJSONArray("eps")) { for (Object o : eps.getJSONArray("eps")) {
JSONObject item = (JSONObject) o; JSONObject item = (JSONObject) o;
json.put("avid", item.getLong("aid")); json.put("avid", item.getLong("aid"));
@ -112,17 +113,86 @@ public class BiliVideo extends Model {
json.put("fnval", 80); json.put("fnval", 80);
json.put("fourk", 1); json.put("fourk", 1);
item.put("title", eps.getString("title") + "$(File.separator)" + item.getString("title")); item.put("title", eps.getString("title") + "$(File.separator)" + item.getString("title"));
list.add(new File(root + StringUtils.toSaveFileName(item.getString("title") + ".mp4")));
downVideo(json, item); downVideo(json, item);
if (downDanmu && merge) {
long tmp = 0;
for (VideoDanMu.DanmakuElem elem : buildDanmuHttp(json.getLong("cid"), json.getLong("avid"), 1)) {
DanmuData danmuData = new DanmuData();
danmuData.setDanmu(elem.getContent());
danmuData.setFontSize(elem.getFontsize());
danmuData.setTime(elem.getProgress() + danmuNextTime);
danmuData.setFontColor(elem.getColor());
danmuData.setModel(elem.getMode());
if (elem.getProgress() > tmp) {
tmp = elem.getProgress();
}
danmuDatas.add(danmuData);
}
danmuNextTime = tmp;
}
}
if (merge) {
merge(root, StringUtils.toSaveFileName(eps.getString("title")), list);
} }
} }
} }
} }
private void merge(String root, String name, List<File> files) {
String saveName = root + name;
File fileList = new File(saveName + File.separator + "tmp.txt");
System.out.println("fileList.getAbsolutePath() = " + fileList.getAbsolutePath());
StringBuilder builder = new StringBuilder();
int i = 0;
List<File> tmp = new ArrayList<>();
for (File file : files) {
System.out.println("file.getName() = " + file.getName());
file.renameTo(new File(file.getParentFile(), i + ".mp4"));
tmp.add(new File(file.getParentFile(), i + ".mp4"));
builder.append("file '").append(i++).append(".mp4'");
builder.append("\n");
}
try {
// boolean b = fileList.createNewFile();
// System.out.println("b = " + b);
FileWriter fw = new FileWriter(fileList);
fw.write(builder.toString());
fw.flush();
fw.close();
String exec = String.format("cd \"%s\" && ffmpeg -f concat -i \"%s\" -c copy %s.mp4", saveName, "tmp.txt", name);
System.out.println("exec = " + exec);
AppTools.exec(exec, new ObjectInterface() {
@Override
public void out(String data) {
super.out(data);
// System.out.println(data);
System.out.println("over");
fileList.delete();
for (File file : tmp) {
file.delete();
}
AssTools tools = new AssTools(name);
tools.addDanmu(danmuDatas);
boolean saveDanmu = tools.saveDanmu(saveName);
System.out.println("弹幕保存:" + saveDanmu);
}
}, false, false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void downVideo(JSONObject json, JSONObject eps) { private void downVideo(JSONObject json, JSONObject eps) {
eps.put("title", StringUtils.toSaveFileName(eps.getString("title"))); eps.put("title", StringUtils.toSaveFileName(eps.getString("title")));
File tmp = new File(HttpTools.downloadPath + eps.getString("title") + ".mp4"); File tmp = new File(HttpTools.downloadPath + eps.getString("title") + ".mp4");
downDanmu(json.getLong("cid"), json.getLong("avid"), eps.getString("title"), 1); if (json.getBooleanValue("danmu") && !json.getBooleanValue("merge")) {
List<VideoDanMu.DanmakuElem> elems = buildDanmuHttp(json.getLong("cid"), json.getLong("avid"), 1);
downDanmu("tmp", eps.getString("title"), elems);
}
if (tmp.exists()) { if (tmp.exists()) {
return; return;
} }
@ -148,19 +218,8 @@ public class BiliVideo extends Model {
} }
} }
private void downDanmu(long cid, long avid, String title, int segment_index) { private void downDanmu(String savePath, String title, List<VideoDanMu.DanmakuElem> danmuList) {
System.out.println("cid = " + cid + ", avid = " + avid + ", title = " + title + ", segment_index = " + segment_index);
JSONObject json = new JSONObject();
json.put("type", 1);
json.put("oid", cid);
json.put("avid", avid);
json.put("segment_index", segment_index);
try { try {
List<VideoDanMu.DanmakuElem> tmp, danmuList = new ArrayList<>();
while (!(tmp = getDanmu(json)).isEmpty()) {
danmuList.addAll(tmp);
json.put("segment_index",++segment_index);
}
AssTools tools = new AssTools(title); AssTools tools = new AssTools(title);
List<DanmuData> list = new ArrayList<>(); List<DanmuData> list = new ArrayList<>();
for (VideoDanMu.DanmakuElem elem : danmuList) { for (VideoDanMu.DanmakuElem elem : danmuList) {
@ -173,13 +232,27 @@ public class BiliVideo extends Model {
list.add(danmuData); list.add(danmuData);
} }
tools.addDanmu(list); tools.addDanmu(list);
tools.saveDanmu("tmp"); boolean saveDanmu = tools.saveDanmu(savePath);
System.out.println("弹幕保存:" + saveDanmu);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
private List<VideoDanMu.DanmakuElem> buildDanmuHttp(long cid, long avid, int segment_index) {
List<VideoDanMu.DanmakuElem> tmp, danmuList = new ArrayList<>();
JSONObject json = new JSONObject();
json.put("type", 1);
json.put("oid", cid);
json.put("avid", avid);
json.put("segment_index", segment_index);
while (!(tmp = getDanmu(json)).isEmpty()) {
danmuList.addAll(tmp);
json.put("segment_index", ++segment_index);
}
return danmuList;
}
private List<VideoDanMu.DanmakuElem> getDanmu(JSONObject json) { private List<VideoDanMu.DanmakuElem> getDanmu(JSONObject json) {
try { try {
byte[] http = BiliBiliUtils.http("https://api.bilibili.com/x/v2/dm/web/seg.so?" + HttpTools.toUrlParams(json), BiliBiliUtils.HTTP.GET, null, BiliBiliUtils.RET_MODEL.BYTE); byte[] http = BiliBiliUtils.http("https://api.bilibili.com/x/v2/dm/web/seg.so?" + HttpTools.toUrlParams(json), BiliBiliUtils.HTTP.GET, null, BiliBiliUtils.RET_MODEL.BYTE);
@ -217,7 +290,6 @@ public class BiliVideo extends Model {
@Override @Override
public void out(String data) { public void out(String data) {
super.out(data); super.out(data);
System.out.println("data = " + data);
videoFile.delete(); videoFile.delete();
audioFile.delete(); audioFile.delete();
} }
@ -265,6 +337,8 @@ public class BiliVideo extends Model {
public static void main(String[] args) { public static void main(String[] args) {
BiliVideo video = new BiliVideo(); BiliVideo video = new BiliVideo();
JSONObject login = new BiliLogin().login();
System.out.println(login);
//岚少 480 //岚少 480
//video.downVideo("https://www.bilibili.com/video/BV1Ps411m7pt?spm_id_from=333.999.0.0"); //video.downVideo("https://www.bilibili.com/video/BV1Ps411m7pt?spm_id_from=333.999.0.0");
//唐诱 合集 //唐诱 合集
@ -279,12 +353,14 @@ public class BiliVideo extends Model {
//video.downVideo("https://www.bilibili.com/video/BV1qF411T7Vf?spm_id_from=444.41.list.card_archive.click"); //video.downVideo("https://www.bilibili.com/video/BV1qF411T7Vf?spm_id_from=444.41.list.card_archive.click");
//唐诱正片 //唐诱正片
//video.downVideo("https://www.bilibili.com/video/BV1L44y147zR?spm_id_from=333.999.0.0");// ep1 //video.downVideo("https://www.bilibili.com/video/BV1L44y147zR?spm_id_from=333.999.0.0");// ep1
//video.downVideo("https://www.bilibili.com/video/BV18L4y1H7rz?spm_id_from=333.999.0.0");// ep5 video.downVideo("https://www.bilibili.com/video/BV18L4y1H7rz?spm_id_from=333.999.0.0", true, false);// ep5
// video.downVideo("https://www.bilibili.com/video/BV1SL411g7FS/?spm_id_from=333.788.recommend_more_video.0"); //all ig 1\5 // video.downVideo("https://www.bilibili.com/video/BV1SL411g7FS/?spm_id_from=333.788.recommend_more_video.0"); //all ig 1\5
// video.downVideo("https://www.bilibili.com/video/BV18L4y1H7rz?spm_id_from=333.999.0.0"); // video.downVideo("https://www.bilibili.com/video/BV18L4y1H7rz?spm_id_from=333.999.0.0");
// video.downVideo("https://www.bilibili.com/video/BV1Pe4y1Q7MX?spm_id_from=444.41.top_right_bar_window_history.content.click");
// int a=16|2048; // int a=16|2048;
// System.out.println("a = " + a); // System.out.println("a = " + a);
//video.downDanmu(428855000L,976216102L,"【都市_情感】《唐可可的诱惑》第一集",1); //video.downDanmu(428855000L,976216102L,"【都市_情感】《唐可可的诱惑》第一集",1);
System.out.println("事件结束");
} }
} }

View File

@ -51,7 +51,7 @@ public class AppTools {
printWriter.close(); printWriter.close();
return writer.toString(); return writer.toString();
} }
public static void exec(String exec, ObjectInterface objectInterface, boolean isOutQQBot, boolean isInput) { public static String exec(String exec, ObjectInterface objectInterface, boolean isOutQQBot, boolean isInput) {
try { try {
Process process; Process process;
if (AppTools.isRuntimeSystemOfWindow()) { if (AppTools.isRuntimeSystemOfWindow()) {
@ -69,17 +69,20 @@ public class AppTools {
} }
); );
} }
String ret;
if (isInput) { if (isInput) {
processOut(process.getInputStream(), objectInterface, isOutQQBot); ret=processOut(process.getInputStream(), objectInterface, isOutQQBot);
processOut(process.getErrorStream(),null,isOutQQBot); processOut(process.getErrorStream(),null,isOutQQBot);
} else { } else {
processOut(process.getErrorStream(), objectInterface, isOutQQBot); ret=processOut(process.getErrorStream(), objectInterface, isOutQQBot);
processOut(process.getInputStream(),null,isOutQQBot); processOut(process.getInputStream(),null,isOutQQBot);
} }
process.destroy(); process.destroy();
return ret;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
return "";
} }
public static boolean isRuntimeSystemOfWindow() { public static boolean isRuntimeSystemOfWindow() {
@ -121,7 +124,7 @@ public class AppTools {
public static void processOut(InputStream inputStream) { public static void processOut(InputStream inputStream) {
processOut(inputStream,null,true); processOut(inputStream,null,true);
} }
public static void processOut(InputStream inputStream, ObjectInterface objectInterface, boolean isOutQQBot){ public static String processOut(InputStream inputStream, ObjectInterface objectInterface, boolean isOutQQBot){
String tmp; String tmp;
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
try { try {
@ -141,6 +144,7 @@ public class AppTools {
if(isOutQQBot) { if(isOutQQBot) {
QQBotManager.getInstance().sendMessage(str.toString()); QQBotManager.getInstance().sendMessage(str.toString());
} }
return str.toString();
} }
public static void sendServer(String title, String msg) { public static void sendServer(String title, String msg) {
try { try {

81
web/bilibili.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>BiliBili下载器</title>
<link rel="stylesheet" href="layui/css/layui.css">
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
</head>
<body>
<div class="myDiy">
<blockquote class="layui-elem-quote">BiliBili下载器
</blockquote>
<br/><br/><br/>
<div class="layui-bg-gray layui-row layui-col-space15" id="card" style="padding: 30px;">
<form class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">视频地址</label>
<div class="layui-input-block">
<input type="url" name="url" required lay-verify="required" placeholder="请输入URL" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">复选框</label>
<div class="layui-input-block">
<input type="checkbox" name="danmu" title="下载弹幕" checked>
<input type="checkbox" name="merge" title="合并合集">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="formDemo">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
</div>
</body>
<script src="layui/layui.js"></script>
<script src="layui/jquery-3.2.1.js"></script>
<script>
layui.use('form', function(){
let form = layui.form;
//监听提交
form.on('submit(formDemo)', function(_data){
//layer.msg(JSON.stringify(data.field));
$.post("/bilibili/down.do",{data:JSON.stringify(_data.field)},function (json) {
layer.msg(json.msg)
})
return false;
});
});
</script>
<style>
.myDiy {
width: 40%;
height: 300px;
margin-top: 10px;
margin-left: 25%;
}
.button {
width: 100px;
height: 100px;
font-size: 1em;
}
</style>
</html>