背景
最近电脑磁盘空间告急,翻查发现b站下载的视频占了快200G,所以就想转移备份到云盘上去,这样还可以在线看。
可是找到存储的下载文件,却发现可视化太低了,文件名都是一串数字,需要通过b站客户端才能解析出视频名称和分P标题这些。
所以在上传云盘之前就得手动处理一下。
回忆
2019年,b站网页版是不提供下载的,当然现在依然不行。
官方下载渠道就是客户端,手机APP或者pc客户端。
PC客户端那个时候只能从win10应用商店下载,哔哩哔哩 UWP。
- UWP即 Windows 10中的Universal Windows Platform简称,即Windows通用应用平台。
- 从腾讯到B站,UWP应用为何被大家弃之如敝履
- 今天登上去看看,挂着停止维护的通知,而且不能登陆,不过功能都还能正常使用。
按着惯例,先百度,淘淘有没有造好的轮子。
有一些博客,但不多。
- 一键批量(解密、合并、整理)B站下载视频,一键批量无损放大视频(1080P→8K),一键批量智能补帧视频(30FPS→240FPS)
- 虽然找到了这个现成的工具,但闲着也是闲着,还是想自己写写
- Bilibili UWP 本地视频文件解析
- B站 bilibili 视频下载方法 批量重命名工具
- 使用JAVA合并哔哩哔哩手机客户端下载的视频
- java实现音视频的合并【主要参考】
下载FFmpeg
UWP 下载的视频格式一直在变化,分段flv–>音画分离–>完整视频MP4。
涉及到视频合并(分段合并、音频视频合并)和格式转换,需要使用 FFmpeg。
进入后点击下载按钮
选择windows后,点击第一个
进入后点击ffmpeg-git-full.7z版下载压缩包
绿色版,解压即可使用
配置环境变量PATH:bin目录
- 如果不配环境变量,就需要写绝对路径调用
检查是否成功
cmd窗口,键入ffmpeg -version命令,如果弹出ffmpeg的版本即为安装成功
分析3种视频格式
- .dvi json格式文件,Title 值即视频名称
- .info json格式文件,Title 值即分P名称
- .xml xml格式文件,弹幕信息
根目录
1.分段flv
- 一般6分钟一段
2.音画分离
3.完整的视频
java代码:BilibiliUtilVideoUWP
package Bilibili;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import util.PythonUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import static json.ParseJsonUtil.readFileContent;
public class BilibiliUtilVideoUWP {
public static void main(String[] args) throws Exception {
// 根目录下 967612226.dvi cover.jpg desktop.ini
// String filepath = "D:\\bilibiliload\\333024338"; // 完整视频MP4
// String filepath = "D:\\bilibiliload\\967612226"; // 音画分开MP4
// String filepath = "D:\\bilibiliload\\17222411"; // 分段flv 一般6分钟一段
File file = new File(filepath);
if (!file.exists() || !file.isDirectory()) {
System.out.println("请指定正确的目录");
return;
}
File[] files = file.listFiles();
// 根目录下获取视频名称 .dvi
File[] dviFiles = file.listFiles((dir, name) -> name.endsWith(".dvi"));
if (dviFiles == null || dviFiles.length == 0) {
System.out.println("不存在指定后缀的文件");
return;
}
File dviFile = dviFiles[0];
if (!dviFile.isFile()) {
System.out.println("不存在指定后缀的文件");
return;
}
String jsonString = readFileContent(dviFile);
JSONObject jsonObject = JSON.parseObject(jsonString);
String title = jsonObject.getString("Title");
if (StringUtils.isBlank(title)) {
System.out.println("视频名称为null");
return;
}
// 复制到一个新的目录下
// File parentFile = file.getParentFile();
File targetFile = new File(file, title);
// 已存在同名目录处理,在文件夹名称后面+副本
while (true) {
if (!targetFile.exists() || !targetFile.isDirectory()) {
// 创建目录
// targetFile.mkdir(); // 只能创建一层目录
targetFile.mkdirs(); // 创建多层目录,包括创建必需但不存在的父目录
break;
}
title += "-副本";
targetFile = new File(file, title);
}
// stream流排序 https://blog.csdn.net/qq_36763236/article/details/111469653
List<File> fileList = Arrays.stream(files)
.filter(f -> isP(f))
.sorted(Comparator.comparing(f -> Integer.parseInt(f.getName())))
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(fileList)) {
for (File dir : fileList) {
// 获取分P标题
// 333024338.info 333024338_1.xml 333024338_1_0.mp4
File[] pFiles = dir.listFiles();
// 视频文件 mp4 flv
List<File> videoFileList = new ArrayList<>();
// 视频信息文件 .info
File partNameFile = null;
for (File pFile : pFiles) {
if (!pFile.isFile()) {
continue;
}
if (pFile.getName().endsWith(".info")) {
partNameFile = pFile;
}
if (pFile.getName().endsWith(".mp4") || pFile.getName().endsWith(".flv")) {
// audio1.mp4 video.mp4 音频和视频都是MP4
videoFileList.add(pFile);
}
}
if (partNameFile != null && CollectionUtils.isNotEmpty(videoFileList)) {
String partNameJsonString = readFileContent(partNameFile);
JSONObject partNameJsonObject = JSON.parseObject(partNameJsonString);
// 分p名称
String partName = partNameJsonObject.getString("PartName");
// 视频格式 1:flv 2:MP4
int format = partNameJsonObject.getIntValue("Format");
// 视频文件个数 flv:未合并 MP4:音频视频分离
int totalParts = partNameJsonObject.getIntValue("TotalParts");
// 是否合并
Boolean isMerged = partNameJsonObject.getBoolean("IsMerged");
if (format == 2 && isMerged != null && isMerged) {
// 完整视频 Mp4
if (StringUtils.isNotBlank(partName)) {
File videoFile = videoFileList.get(0);
// copy文件
// 4种Java文件复制的方法 https://blog.csdn.net/qq_30436011/article/details/127490081
File dest = new File(targetFile, partName + ".mp4");
Files.copy(videoFile.toPath(), dest.toPath());
// 测试时你,可以先只处理一个
// break;
}
}
if (format == 2 && isMerged != null && !isMerged) {
// 音频视频分离
if (StringUtils.isNotBlank(partName)) {
if (videoFileList.size() > 2) {
System.out.println("视频文件存在多个: " + dir.getAbsolutePath());
continue;
}
// audio1.mp4 video.mp4
String audioPath = null; // 音频文件路径
String videoPath = null; // 视频文件路径
File file1 = videoFileList.get(0);
File file2 = videoFileList.get(1);
if (file1.getName().startsWith("audio")) {
audioPath = file1.getAbsolutePath();
videoPath = file2.getAbsolutePath();
} else if (file1.getName().startsWith("video")) {
audioPath = file2.getAbsolutePath();
videoPath = file1.getAbsolutePath();
}
File dest = new File(targetFile, partName + ".mp4");
// 合并音视频
ffmpegMerge(videoPath, audioPath, dest.getAbsolutePath());
// 测试时你,可以先只处理一个
// break;
}
}
// ffmpeg将多个flv文件合成为mp4
if (format == 1) {
// 分段flv
if (StringUtils.isNotBlank(partName)) {
// 17222411_2_0.flv 17222411_2_1.flv 17222411_2_2.flv
videoFileList = videoFileList.stream()
.sorted(Comparator.comparing(f -> Integer.parseInt(f.getName().substring(f.getName().indexOf("_") + 1, f.getName().lastIndexOf(".")).replace("_", ""))))
.collect(Collectors.toList());
String str = "";
for (File flvFile : videoFileList) {
if (StringUtils.isNotBlank(str)) {
str += "\n";
}
str += "file '" + flvFile.getAbsolutePath() + "'";
}
System.out.println("flv个数:" + videoFileList.size());
// 保存文件名到目标文件 给ffmpeg调用
File listFile = new File(file, "list.txt");
String listFilePath = listFile.getAbsolutePath();
// 是覆盖,还是追加??? 是覆盖
writeFile(str, listFilePath);
File dest = new File(targetFile, partName + ".mp4");
ffmpegMergeFlv(listFilePath, dest.getAbsolutePath());
// 测试时你,可以先只处理一个
// break;
}
}
}
}
}
}
/**
* 是否为分P目录
*/
private static boolean isP(File file) {
if (file == null || !file.isDirectory()) {
return false;
}
String name = file.getName();
// java判断字符串是否为纯数字 https://jingyan.baidu.com/article/c74d6000d721f50f6b595d5e.html
return name.matches("[0-9]+");
}
/**
* 将内容写到目标文件
*/
private static void writeFile(String newContent, String targetFile) throws Exception {
File file = new File(targetFile);
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write(newContent);
osw.flush();
osw.close();
fos.close();
}
/**
* java实现音视频的合并
* https://blog.csdn.net/maziaotong/article/details/127863727
* 使用 ffmpeg 来合并音频和视频文件 https://www.cnblogs.com/dragona/p/17206097.html
*
* @param videoFilePath 原视频路径
* @param audioFilePath 原视频路径
* @param desFilePath 合并后视频存放路径+视频名称
*/
public static void ffmpegMerge(String videoFilePath, String audioFilePath, String desFilePath) {
// ffmpeg -i 原视频路径 -i 原音频路径 -codec copy 合并后视频存放路径+视频名称
// 解释器
String exe = "ffmpeg";
// 组合成一个字符串数组
String[] cmdArr = new String[]{exe, "-i", videoFilePath, "-i", audioFilePath, "-codec", "copy", desFilePath};
PythonUtil.executePythonScript(cmdArr);
}
/**
* 使用ffmpeg批量合并flv文件
* ffmpeg -f concat -i mylist.txt -c copy output.flv
* https://www.likecs.com/show-379618.html
*
* @param desFilePath 合并后视频存放路径+视频名称
*/
public static void ffmpegMergeFlv(String listFilePath, String desFilePath) {
// 解决报错:-safe 0
// [concat @ 000001bbe578d3c0] Unsafe file name 'D:bilibiliload17222411117222411_1_0.flv'
// [in#0 @ 000001bbe5777500] Error opening input: Operation not permitted
// 解决报错:文件路径加单引号
// Impossible to open 'D:bilibiliload17222411117222411_1_0.flv'
// -loglevel quiet 如果报错可以删掉,放出日志
// ffmpeg设置终端不显示日志 https://www.cnblogs.com/chentiao/p/17174382.html
String cmdStr = MessageFormat.format("ffmpeg -loglevel quiet -f concat -safe 0 -i {0} -codec copy {1}", listFilePath, desFilePath);
System.out.println("cmdStr: " + cmdStr);
PythonUtil.executeCmd(cmdStr);
}
}
最新版
老版的处理完,顺便研究下新版的客户端
pc客户端
java代码:BilibiliUtilVideoWindows
下载
使用
下载完,客户端是显示的合集,本地文件夹却是一个分P一个文件夹,没有归类,全在根目录
另外,视频格式换成了m4s,音画分离,而且你还无法判断谁是音频、谁是视频
andriod
java代码:BilibiliUtilVideoAPP
缓存的文件在 Android/data/tv.danmaku.bili/download
文件有按合集归类,也是音画分离,不过视频类别一目了然