环境jdk14
maven构建
接口文档地址:https://github.com/1015770492/bilibili-download/blob/master/doc/bilibili-Api文档.md
原理是下载视频文件(不带声音)和音频文件,然后通过第三方工具ffmpeg
合并视频文件和音频文件为一个文件
ffmpeg官网地址
自定义的http请求工具类部分代码展示;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BiliBiliHttpUtils {
/**
* 没有就创建目录,有则返沪true
* @param destDirName
* @return
*/
public static boolean createDirect(String destDirName) {
File file = new File(destDirName);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
return true;
}
/**
* 下载文件的作用,需要3个参数 资源原路径,真正下载的url,保存本地的路径
*
* @param refererUrl 视频的参考路径,
* @param url 真正下载的url链接,可以是m4s文件和audio文件,下载后需要合并
* @param savePath 保存的路径
* @return 下载成功则返回true,中间报异常退出则返回false
*/
public static boolean downloadFile(String refererUrl, String url, String savePath) {
System.out.print("下载文件:");
System.out.println(savePath);//打印保存的文件路径
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("referer", refererUrl);//设置请求头
HttpEntity<String> entity = new HttpEntity<String>("", headers);
System.out.println("发送请求获取文件的输入流");
ResponseEntity<Resource> in = restTemplate.exchange(url, HttpMethod.GET, entity, Resource.class);
if (in.getStatusCode() == HttpStatus.OK) {
try (InputStream is = in.getBody().getInputStream()) {//java9新特性自动关闭流
boolean flag = createDirect(savePath);
if (flag) {
try (FileOutputStream fos = new FileOutputStream(savePath);) {
System.out.println("正在写入文件...");
is.transferTo(fos);//写入输出流
return true;//写入成功返回true
} catch (IOException e) {
e.printStackTrace();
System.out.println("写入中断");
}
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("创建文件夹失败");
}
System.out.println("文件写入完成");
} else {
System.out.println("状态码" + in.getStatusCode());
System.out.println(in.toString());
}
return false;//默认返回失败
}
/**
* 获取url中的 bvid
*
* @param url
* @return
* @throws Exception
*/
public static String getBVID(String url) throws Exception {
Pattern BVIDPartPattern = Pattern.compile("BV[a-z|0-9|A-Z]*");
Matcher matcher = BVIDPartPattern.matcher(url);
String bvid;
if (matcher.find()) {
bvid = matcher.group();
} else {
throw new Exception("传入的url中不包含bvid");
}
return bvid;
}
/**
* 传入视频的播放地址,例如url: "https://www.bilibili.com/video/BV1m4411H7pi" 获取其中包含BV的这串字符串
*
* @param url
* @return
* @throws Exception
*/
public static JSONObject getCidJSON(String url) throws Exception {
String bvid = getBVID(url);
//从url中转换成最终请求的url接口
String requestUrl = new StringBuilder("http://api.bilibili.com/x/web-interface/view?bvid=").append(bvid).toString();
String jsonString = new RestTemplate().getForEntity(requestUrl, String.class).getBody();
JSONObject parse = JSONObject.parseObject(jsonString);
return parse.getJSONObject("data");//返回data部分
}
/**
* 传入cid的json数组,获取所有下载链接--》一个cid对应一个视频,一个视频有多个清晰度的播放源
*
* @param cidJSONObject
* @return
*/
public static List<JSONObject> getPlayUrlList(JSONObject cidJSONObject) {
String bvid = cidJSONObject.getString("bvid");//视频的id
String title = cidJSONObject.getString("title");//标题--》用来做文件夹
String picUrl = cidJSONObject.getString("pic");//封面图片
JSONArray pages = cidJSONObject.getJSONArray("pages");//包含这一系列视频的cid内容
//如果传入的cid过多并行流会导致http请求过大,容易被服务器拒绝
try (Stream<JSONObject> jsonObjectStream = pages.stream()
.map(o -> {
JSONObject obj = (JSONObject) o;
String cid = obj.getString("cid");//获取cid
String part = obj.getString("part");//获取视频名 前缀
System.out.println(part);//映射
//获取播放地址的api
String requestUrl = new StringBuilder("https://api.bilibili.com/x/player/playurl?cid=").append(cid)
.append("&bvid=").append(bvid).append("&qn=80&fnver=0&fnval=16&fourk=1").toString();
String jsonString = new RestTemplate().getForEntity(requestUrl, String.class).getBody();//执行太快了
JSONObject data = JSONObject.parseObject(jsonString).getJSONObject("data");//获取api返回的jaon中的data对象
/**
* 添加自定义的属性,方便保存
*/
data.put("cid", cid);//将cid添加到返回的数据中
data.put("directName", title);//添加视频的标题作为文件夹
data.put("videoName", part);//添加视频的名称
return data;//将添加了自定义属性的json返回,作为新的流
})) {
// System.out.println(cidJSONObject);
return jsonObjectStream.collect(Collectors.toList());//将收集到的url封装起来
}
}
}
合并图像和音频文件代码展示
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
public class MergeVideoAndAudioUtils {
static ExecutorService executorService = Executors.newFixedThreadPool(20);
public static boolean merge(String videoPath, String audioPath, String savePath) throws ExecutionException, InterruptedException {
File directory = new File("");//设定为当前文件夹
String currentAbsolutePath = directory.getAbsolutePath();
// System.out.println(currentAbsolutePath);
String relativePath = "src/main/java/top/huashengshu/bilibili/utils/merge/util/ffmpeg.exe";
String ffmpegAbsolutePath = new StringBuilder(currentAbsolutePath).append("/").append(relativePath)
.append(" -i \"").append(videoPath)
.append("\" -i \"").append(audioPath)
.append("\" -c copy \"").append(savePath).append("\"")
.toString();//合并音视频文件得命令
CompletableFuture<Process> processFuture = CompletableFuture.supplyAsync(() -> {
Process process = null;
try {
Runtime runtime = Runtime.getRuntime();//获取cmd窗口
System.out.println(ffmpegAbsolutePath);//打印一下命令
process = runtime.exec(ffmpegAbsolutePath);//执行合并命令
} catch (IOException e) {
e.printStackTrace();
return process;//异常返回
}
return process;
}, executorService);
Process process = processFuture.get();//获取上一个的process对象
CompletableFuture.runAsync(() -> {
while (process.isAlive()) {
}//判断合并命令是否执行完成,如果完成则会跳出循环
clearGarbageFile(videoPath,savePath);//清理垃圾文件
clearGarbageFile(audioPath,savePath);//清理垃圾文件
},executorService);
return true;//将合并结果返回
}
public static boolean clearGarbageFile(String filePath,String mp4Path) {
File mp4File = new File(mp4Path);
if (mp4File.exists()) {
File tempFile = new File(filePath);
if (tempFile.exists()){
tempFile.delete();//文件存在,删除文件
}else {
System.out.println("文件不存在:"+filePath);
}
}else {
System.out.println("没有合并成:"+mp4Path);
}
return true;
}
}
调用BiliBiliUtils
的patchDownload
方法即可
/**
* 测试案例
*
* @param args
*/
public static void main(String[] args) {
String referUrl = "https://www.bilibili.com/video/BV1oi4y1u7Bq";//测试用的url
BiliBiliUtils.patchDownload(referUrl, "D:/");//将referUrl系列的所有视频存到D:/盘
}
项目没有完全完成,在改进中,并且会有后期版本,采用vue+ElementUI的静态页面,调用工具类实现批量下载视频,暂时没有实现登录功能