文章目录

  • 前言
  • 一、源码
  • 二、使用步骤
  • 总结



前言

之前写过一个只能合成的m4s的版本,参考C++调用ffmpeg批量合并bilibili缓存视频,实际使用存在很多bug,先以及进行优化,具体如下:

1、增加blv格式的视频合成,实际缓存的视频是m4s和blv混合的,上一版的合成不了blv;
2、优化遍历算法,之前的遍历查找有问题,现在完美过滤未缓存的视频;
3、过滤部分遇到的特殊字符,部分特殊字符会导致合成失败;
4、支持官方动漫视频合成;

一、源码

示例源码如下:

#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <thread>
#include <windows.h>
#include <string.h>
#include <io.h>
#include <direct.h>

// 这里是我将jsoncpp封装成一个库了,方便使用
#include "../jsontool/jsontool.h"
#pragma comment(lib, "JsonTool.lib")

using namespace std;

#define AudioName		"audio.m4s"
#define VideoName		"video.m4s"
#define JsonName		"entry.json"
#define BLVVideo		".blv"

vector<string> g_vecTitleNames;
vector<string> g_vecPartNames;
vector<string> g_vecVideoPaths;
vector<string> g_vecAudioPaths;

#define ERR_NO_FIND		-1

int GetTitleAndPage(const char* pszJsonPath, char* pszTitle, char* pszPage);

int FindJson(const char* pszVideoPath, char* pszTitle, char* pszPage)
{
	// 通过json文件路径获取title和part
	char szJsonPath[MAX_PATH] = { 0 };
	int iLen = strlen(pszVideoPath);
	strcpy(szJsonPath, pszVideoPath);
	// 阶段最后面的 '\'
	szJsonPath[iLen - 1] = '\0';
	// 查找并截断路径到上一层目录
	char* pFind = strrchr(szJsonPath, '\\');
	if (pFind == NULL)
	{
		return -1;
	}
	pFind[1] = '\0';
	strcat(szJsonPath, JsonName);
	int iRet = GetTitleAndPage(szJsonPath, pszTitle, pszPage);
	if (iRet != 0)
	{
		return -1;
	}
	return 0;
}

int FindFile(const char* pszPath)
{
	int iRet = 0;
	char szFindPath[MAX_PATH] = { 0 };
	DWORD dwFileAttributes;
	string strFileName;
	WIN32_FIND_DATA wfd;
	sprintf(szFindPath, "%s*.*", pszPath);
	HANDLE hFindFile = ::FindFirstFile(szFindPath, &wfd);
	if (hFindFile == INVALID_HANDLE_VALUE)
	{
		// 没有找到任何文件
		return ERR_NO_FIND;
	}

	// 找到文件,开始遍历
	strFileName = wfd.cFileName;
	while (strFileName.size() > 0)
	{
		// 过滤 . 和 ..
		if (strFileName != "." && strFileName != "..")
		{
			dwFileAttributes = wfd.dwFileAttributes;
			if (dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY)	// 目录
			{
				// 如果是目录,则继续递归查找
				// 查找json所在路径
				char szSubFindPath[MAX_PATH] = { 0 };
				sprintf(szSubFindPath, "%s%s\\", pszPath, strFileName.c_str());
				iRet = FindFile(szSubFindPath);
				if (iRet != 0)
				{
					break;
				}
			}
			else if (dwFileAttributes == FILE_ATTRIBUTE_ARCHIVE) // 文件
			{
				// 防止video没有缓存完,json文件肯定有,video不一定,所以先找到video,再读上一层的json,vedio没缓存完,就没必要读json
				if (strFileName == VideoName)
				{
					// video找到了,先返回上一层读json,再通过json文件路径获取title和part
					char szTitleName[MAX_PATH] = { 0 };
					char szPartName[MAX_PATH] = { 0 };
					iRet = FindJson(pszPath, szTitleName, szPartName);
					if (iRet != 0)
					{
						break;
					}
					// 插入title和章节名
					g_vecTitleNames.push_back(szTitleName);
					g_vecPartNames.push_back(szPartName);
					// 插入video路径
					g_vecVideoPaths.push_back(string(pszPath) + VideoName);
					g_vecAudioPaths.push_back(string(pszPath) + AudioName);
					break;
				}
				else if (strFileName.find(BLVVideo) != string::npos)
				{
					// blv格式,需要特殊处理,将文件名写到txt中
					string strBlvTxtFile = pszPath;
					strBlvTxtFile += "blv.txt";

					// 可能存在多个*.blv文件,只要插入一次即可
					// 将当前所在路径和上一次插入vector的路径进行匹配,判断是否是第一次
					string strLastVideoPath = g_vecVideoPaths.back();
					if (strLastVideoPath.find(pszPath) == string::npos)
					{
						// 没找到,则是第一次进入
						// 删除测试生成的文件
						DeleteFile(strBlvTxtFile.c_str());

						// video找到了,先返回上一层读json,再通过json文件路径获取title和part
						char szTitleName[MAX_PATH] = { 0 };
						char szPartName[MAX_PATH] = { 0 };
						iRet = FindJson(pszPath, szTitleName, szPartName);
						if (iRet != 0)
						{
							break;
						}
						// 插入title和章节名
						g_vecTitleNames.push_back(szTitleName);
						g_vecPartNames.push_back(szPartName);

						// 简单处理,插入相同路径作为blv格式的判断依据
						g_vecAudioPaths.push_back(strBlvTxtFile);
						g_vecVideoPaths.push_back(strBlvTxtFile);
					}

					std::ofstream fout;
					fout.open(strBlvTxtFile, std::ios::app);
					fout << "file '" << pszPath << strFileName << "'" << endl;
					fout.close();
				}
			}
		}
		// 查找下一个文件
		if (!::FindNextFile(hFindFile, &wfd))
		{
			break;
		}
		strFileName = wfd.cFileName;
	}
	::FindClose(hFindFile);
	return iRet;
}

string UnicodeToANSI(const wstring& str)
{
	char*     pElementText;
	int    iTextLen;
	// wide char to multi char
	iTextLen = WideCharToMultiByte(CP_ACP,
		0,
		str.c_str(),
		-1,
		NULL,
		0,
		NULL,
		NULL);
	pElementText = new char[iTextLen + 1];
	memset((void*)pElementText, 0, sizeof(char) * (iTextLen + 1));
	::WideCharToMultiByte(CP_ACP,
		0,
		str.c_str(),
		-1,
		pElementText,
		iTextLen,
		NULL,
		NULL);
	string strText;
	strText = pElementText;
	delete[] pElementText;
	return strText;
}

wstring UTF8ToUnicode(const string& str)
{
	int  len = 0;
	len = str.length();
	int  unicodeLen = ::MultiByteToWideChar(CP_UTF8,
		0,
		str.c_str(),
		-1,
		NULL,
		0);
	wchar_t *  pUnicode;
	pUnicode = new  wchar_t[unicodeLen + 1];
	memset(pUnicode, 0, (unicodeLen + 1)*sizeof(wchar_t));
	::MultiByteToWideChar(CP_UTF8,
		0,
		str.c_str(),
		-1,
		(LPWSTR)pUnicode,
		unicodeLen);
	wstring  rt;
	rt = (wchar_t*)pUnicode;
	delete  pUnicode;

	return  rt;
}

void removeSpecialChar(string& strValue)
{
	int iFindIndex = 0;
	// 去掉空格
	while ((iFindIndex = strValue.find(" ")) != string::npos)
	{
		strValue.replace(iFindIndex, 1, "");
	}

	// 将 & 替换成 与,不然ffmpeg会报错
	while ((iFindIndex = strValue.find("&")) != string::npos)
	{
		strValue.replace(iFindIndex, 1, "与");
	}

	// 去掉斜杠'/'
	while ((iFindIndex = strValue.find("/")) != string::npos)
	{
		strValue.replace(iFindIndex, 1, "");
	}

	// 去掉反斜杠'\'
	while ((iFindIndex = strValue.find("\\")) != string::npos)
	{
		strValue.replace(iFindIndex, 1, "");
	}
}

int GetTitleAndPage(const char* pszJsonPath, char* pszTitle, char* pszPage)
{
	string strValue;
	char szValue[4096] = { 0 };
	std::ifstream is;
	is.open(pszJsonPath, std::ios::binary);
	while (!is.eof())
	{
		memset(szValue, 0, sizeof(szValue));
		is.getline(szValue, sizeof(szValue));
		strValue += szValue;
	}

	CJsonTool json;
	int iRet = json.InitJson(strValue.c_str());
	//cout << "InitJsonStr: " << iRet << endl;
	char szBuff[MAX_PATH] = { 0 };
	iRet = json.GetStr("page_data", "part", szBuff, MAX_PATH);
	if (iRet != 0)
	{
		// up主上传的视频,json文件存放的节点是page_data/part,缓存官方动漫节点是ep/index_title
		iRet = json.GetStr("ep", "index_title", szBuff, MAX_PATH);
	}

	if (iRet != 0)
	{
		is.close();
		return iRet;
	}

	strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));
	
	// 去掉特殊字符
	removeSpecialChar(strValue);
	
	//cout << "GetJsonStr: " << iRet << "	" << strValue << endl;

	strcpy(pszPage, strValue.c_str());

	iRet = json.GetStr("", "title", szBuff, MAX_PATH);
	if (iRet != 0)
	{
		is.close();
		return iRet;
	}

	strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));

	// 去掉特殊字符
	removeSpecialChar(strValue);

	//cout << "GetJsonStr: " << iRet << "	" << strValue << endl;

	strcpy(pszTitle, strValue.c_str());

	is.close();

	return 0;
}

void GenerateVideo(const char* pPath)
{
	if (g_vecAudioPaths.size() == 0 || 
		g_vecAudioPaths.size() != g_vecVideoPaths.size() ||
		g_vecAudioPaths.size() != g_vecTitleNames.size() ||
		g_vecAudioPaths.size() != g_vecPartNames.size())
	{
		cout << "查找视频资源失败" << endl;
		return;
	}

	cout << "--------------------开始合成-------------------------" << endl;
	char szSavePath[MAX_PATH] = { 0 };
	char szSvaeFileName[MAX_PATH] = { 0 };
	char szCommand[1024] = { 0 };
	auto funGenerate = [](string strCommand) 
	{
		system(strCommand.c_str());
	};
	for (int i = 0; i < g_vecTitleNames.size(); i++)
	{
		// 跳过空
		if (g_vecAudioPaths[i].size() == 0 ||
			g_vecVideoPaths[i].size() == 0 ||
			g_vecTitleNames[i].size() == 0 ||
			g_vecPartNames[i].size() == 0)
		{
			continue;
		}
		// 创建保存文件夹
		sprintf(szSavePath, "%s%s\\", pPath, g_vecTitleNames[i].c_str());
		if (_access(szSavePath, 0) == -1)		// 如果文件夹不存在
		{
			_mkdir(szSavePath);
		}

		// 保存文件名
		sprintf(szSvaeFileName, "%s%s.mp4", szSavePath, g_vecPartNames[i].c_str());

		// 组装命令
		// 不相等则为m4s文件
		if (g_vecVideoPaths[i] != g_vecAudioPaths[i])
		{
			// ffmpeg.exe -i video.m4s -i audio.m4s -codec copy name
			sprintf(szCommand, "%sffmpeg.exe -i %s -i %s -codec copy %s",
				pPath, g_vecVideoPaths[i].c_str(), g_vecAudioPaths[i].c_str(), szSvaeFileName);
		}
		else // 相等则为blv文件
		{
			// ffmpeg.exe -f concat -i blv.txt -c copy name
			sprintf(szCommand, "%sffmpeg.exe -f concat -safe 0 -i %s -c copy %s",
				pPath, g_vecVideoPaths[i].c_str(), szSvaeFileName);
		}
		cout << szCommand << endl;
		// 使用线程处理
		std::thread t(funGenerate, szCommand);
		t.join();

		cout << i << "	---	" << "输出:" << szSvaeFileName << endl;
	}
}
int main()
{
	// 获取当前模块(exe)所在路径
	char szModuleFileName[MAX_PATH] = { 0 };
	::GetModuleFileName(NULL, szModuleFileName, MAX_PATH);
	// 查找目录
	char szFindPath[MAX_PATH] = { 0 };
	strcpy(szFindPath, szModuleFileName);
	char *pPos = strrchr(szFindPath, '\\');
	if (pPos == NULL)
	{
		return -1;
	}
	// 截断文件名
	pPos[1] = '\0';

	FindFile(szFindPath);
	
	GenerateVideo(szFindPath);

	system("pause");

    return 0;
}

二、使用步骤

直接将手机bilibili的缓存目录下download文件夹拷贝到当前文件夹下,双击Bilibili.exe即可自动合并生成

总结

jsontool没法提供,可直接下载jsoncpp进行替换该功能,最终生成的执行文件上传审核不过,不知道为啥?有时间上传百度云再分享吧!