Files
Bilby/SubtitleShifter.cpp
amass a842d429b7
Some checks failed
Deploy Applications / PullDocker (push) Failing after 6m52s
Deploy Applications / Build (push) Failing after 2s
Windows CI / build (push) Has been cancelled
add tool.
2025-10-22 21:51:15 +08:00

240 lines
7.2 KiB
C++

#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>
struct Subtitle {
std::string layer;
std::string startTime;
std::string endTime;
std::string style;
std::string name;
std::string marginL;
std::string marginR;
std::string marginV;
std::string effect;
std::string text;
std::string originalLine;
bool isDialogue;
};
std::size_t findNthCharacter(const std::string &str, char character, int n) {
std::size_t pos = -1;
for (int i = 0; i < n; ++i) {
pos = str.find(character, pos + 1);
if (pos == std::string::npos) {
return std::string::npos;
}
}
return pos;
}
/**
* @brief 将时间字符串转换为毫秒
*
* @param timeStr
* @return int
*/
int timeToMilliseconds(const std::string &timeStr) {
int hours, minutes, seconds, hundredths;
char colon, dot;
std::istringstream iss(timeStr);
iss >> hours >> colon >> minutes >> colon >> seconds >> dot >> hundredths;
return (hours * 3600 + minutes * 60 + seconds) * 1000 + hundredths * 10;
}
/**
* @brief 将毫秒转换为时间字符串
*
* @param ms
* @return std::string
*/
std::string millisecondsToTime(int ms) {
if (ms < 0) ms = 0;
int totalSeconds = ms / 1000;
int hundredths = (ms % 1000) / 10;
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int seconds = totalSeconds % 60;
std::ostringstream oss;
oss << hours << ":" << (minutes < 10 ? "0" : "") << minutes << ":" << (seconds < 10 ? "0" : "") << seconds << "."
<< (hundredths < 10 ? "0" : "") << hundredths;
return oss.str();
}
/**
* @brief 偏移单个字幕的时间
*
* @param sub
* @param offsetMs
* @return Subtitle
*/
Subtitle shiftSubtitle(const Subtitle &sub, int offsetMs) {
if (!sub.isDialogue) {
return sub;
}
Subtitle shifted = sub;
// 转换开始时间并偏移
int startMs = timeToMilliseconds(sub.startTime);
startMs += offsetMs;
shifted.startTime = millisecondsToTime(startMs);
// 转换结束时间并偏移
int endMs = timeToMilliseconds(sub.endTime);
endMs += offsetMs;
shifted.endTime = millisecondsToTime(endMs);
// 重新构建行
std::ostringstream oss;
oss << "Dialogue: " << shifted.layer << "," << shifted.startTime << "," << shifted.endTime << "," << shifted.style
<< "," << shifted.name << "," << shifted.marginL << "," << shifted.marginR << "," << shifted.marginV << ","
<< shifted.effect << "," << shifted.text;
shifted.originalLine = oss.str();
return shifted;
}
Subtitle parseLine(const std::string &line) {
constexpr std::string_view dialogue = "Dialogue:";
Subtitle sub;
sub.originalLine = line;
sub.isDialogue = false;
// 检查是否是Dialogue行
if (line.find(dialogue) == 0) {
sub.isDialogue = true;
std::size_t textStart = findNthCharacter(line, ',', 9);
if ((textStart != std::string::npos) && (textStart < line.length())) {
sub.text = line.substr(textStart + 1);
std::string attr = line.substr(dialogue.length(), textStart - dialogue.length());
boost::algorithm::trim(attr);
std::vector<std::string> splits;
boost::algorithm::split(splits, attr, boost::is_any_of(","), boost::algorithm::token_compress_off);
sub.layer = splits[0];
sub.startTime = splits[1];
sub.endTime = splits[2];
sub.style = splits[3];
sub.name = splits[4];
sub.marginL = splits[5];
sub.marginR = splits[6];
sub.marginV = splits[7];
sub.effect = splits[8];
}
}
return sub;
}
std::string generateTargetFilename(const std::string &filename, const std::string &prefix, const std::string &suffix) {
auto start = filename.find(prefix);
auto end = filename.find(suffix);
return filename.substr(start + prefix.length(), end - (start + prefix.length()));
}
bool processASSFile(const std::filesystem::path &inputPath, std::filesystem::path &outputPath, int offsetMs) {
std::ifstream ifs(inputPath);
std::ofstream ofs(outputPath);
if (!ifs.is_open()) {
std::cerr << "无法打开输入文件: " << inputPath << std::endl;
return false;
}
if (!ofs.is_open()) {
std::cerr << "无法创建输出文件: " << outputPath << std::endl;
return false;
}
std::string line;
while (std::getline(ifs, line)) {
Subtitle sub = parseLine(line);
if (sub.isDialogue) {
Subtitle shifted = shiftSubtitle(sub, offsetMs);
ofs << shifted.originalLine << std::endl;
} else {
ofs << line << std::endl;
}
}
return true;
}
void processDirectory(const std::filesystem::path &source, const std::filesystem::path &target, int offsetMs) {
if (!std::filesystem::exists(source)) {
std::cerr << "错误: 源目录不存在: " << source << std::endl;
return;
}
if (!std::filesystem::exists(target)) {
std::cout << "创建目标目录: " << target << std::endl;
if (!std::filesystem::create_directories(target)) {
std::cerr << "错误: 无法创建目标目录: " << target << std::endl;
return;
}
}
std::vector<std::filesystem::path> assFiles;
try {
for (const auto &entry : std::filesystem::directory_iterator(source)) {
if (entry.is_regular_file() && entry.path().extension() == ".ass") {
assFiles.push_back(entry.path());
}
}
} catch (const std::filesystem::filesystem_error &ex) {
std::cerr << "文件系统错误: " << ex.what() << std::endl;
return;
}
if (assFiles.empty()) {
std::cout << "在目录 " << source << " 中未找到.ass文件" << std::endl;
return;
}
std::cout << "找到 " << assFiles.size() << " 个.ass文件" << std::endl;
for (const auto &sourceFile : assFiles) {
// 生成目标文件名
std::string targetFilename = generateTargetFilename(
sourceFile.filename().string(), "MONSTER.TV.2004.DVDRip-Hi.x264.AC3.1280.EP", "-nezumi.zh_CN");
// 确保文件扩展名为.ass
if (std::filesystem::path(targetFilename).extension() != ".ass") {
targetFilename += ".ass";
}
std::filesystem::path targetFile = target / targetFilename;
// std::cout << sourceFile << " --> " << targetFile << std::endl;
// 处理文件
processASSFile(sourceFile, targetFile, offsetMs);
}
}
// 1. 找出指定目录下后缀为 `.ass` 下的文件
// 2. 如果目标文件夹不存在,则创建目标文件夹
// 3. 对每个源文件,提取文件名,然后按照模板提出字串,和目标文件夹拼凑成新的目标文件
int main(int argc, char const *argv[]) {
processDirectory("/mnt/e/Downloads/[X2] MONSTER 2004 [Chs Subtitle]",
"/mnt/e/Downloads/[X2] MONSTER 2004 [Chs Subtitle]/output",700);
}