Files
MyTool/backend/src/main/java/com/music/service/ConvertService.java
2026-01-29 18:26:02 +08:00

220 lines
8.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.music.service;
import com.music.dto.ProgressMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class ConvertService {
private static final Logger log = LoggerFactory.getLogger(ConvertService.class);
/** 无损格式,需转换为 FLAC */
private static final Set<String> LOSSLESS_EXTENSIONS = new HashSet<>(Arrays.asList(
"wav", "ape", "aiff", "aif", "wv", "tta"
));
/** 有损格式及 FLAC跳过不处理 */
private static final Set<String> SKIP_EXTENSIONS = new HashSet<>(Arrays.asList(
"flac", "mp3", "m4a", "aac", "ogg", "opus", "wma"
));
private static final int FFMPEG_COMPRESSION_LEVEL = 5;
private final SimpMessagingTemplate messagingTemplate;
private final ProgressStore progressStore;
public ConvertService(SimpMessagingTemplate messagingTemplate, ProgressStore progressStore) {
this.messagingTemplate = messagingTemplate;
this.progressStore = progressStore;
}
@Async
public void convert(String taskId, String srcDir, String dstDir, String mode) {
Path srcPath = Paths.get(srcDir);
Path dstPath = Paths.get(dstDir);
try {
// 验证输入目录路径是否填写
if (srcDir == null || srcDir.trim().isEmpty()) {
sendProgress(taskId, 0, 0, 0, 0, null, "输入目录路径不能为空", true);
return;
}
// 验证输入目录是否存在
if (!Files.exists(srcPath) || !Files.isDirectory(srcPath)) {
sendProgress(taskId, 0, 0, 0, 0, null, "输入目录不存在或不是目录", true);
return;
}
// 验证输出目录路径是否填写
if (dstDir == null || dstDir.trim().isEmpty()) {
sendProgress(taskId, 0, 0, 0, 0, null, "输出目录路径不能为空", true);
return;
}
// 移动模式下,输入和输出目录不能相同
if ("move".equalsIgnoreCase(mode) && srcPath.equals(dstPath)) {
sendProgress(taskId, 0, 0, 0, 0, null, "移动模式下,输入目录和输出目录不能相同", true);
return;
}
// 创建输出目录(如果不存在)
if (!Files.exists(dstPath)) {
Files.createDirectories(dstPath);
}
// 扫描输入目录,查找需要转换的文件
List<Path> toConvert = new ArrayList<>();
Files.walkFileTree(srcPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (shouldConvert(file)) {
toConvert.add(file);
}
return FileVisitResult.CONTINUE;
}
});
int total = toConvert.size();
AtomicInteger processed = new AtomicInteger(0);
AtomicInteger success = new AtomicInteger(0);
AtomicInteger failed = new AtomicInteger(0);
log.info("转码任务开始,扫描目录: {}, 待转码文件数: {}", srcPath, total);
sendProgress(taskId, total, 0, 0, 0, null, "扫描完成,开始转码任务...", false);
// 如果目录存在但没有需要转换的文件,正常完成任务
if (total == 0) {
log.info("目录 {} 中未找到需要转码的无损音频文件", srcPath);
sendProgress(taskId, 0, 0, 0, 0, null,
"目录扫描完成未找到需要转码的无损音频文件WAV/APE/AIFF/WV/TTA。目录中可能只有 FLAC 或有损格式文件,已自动跳过。", true);
return;
}
for (Path srcFile : toConvert) {
String fileName = srcFile.getFileName().toString();
String baseName = getBaseName(fileName);
String outFileName = baseName + ".flac";
Path outFile = resolveTargetFile(dstPath, outFileName);
try {
runFfmpeg(srcFile, outFile);
success.incrementAndGet();
if ("move".equalsIgnoreCase(mode)) {
try {
Files.deleteIfExists(srcFile);
} catch (IOException e) {
log.warn("移动模式删除源文件失败: {}", srcFile, e);
}
}
sendProgress(taskId, total, processed.incrementAndGet(),
success.get(), failed.get(), fileName,
"已处理: " + fileName, false);
} catch (Exception e) {
failed.incrementAndGet();
log.warn("转码失败: {} - {}", fileName, e.getMessage());
sendProgress(taskId, total, processed.incrementAndGet(),
success.get(), failed.get(), fileName,
"转码失败: " + fileName + " - " + e.getMessage(), false);
}
}
sendProgress(taskId, total, processed.get(), success.get(), failed.get(),
null, String.format("任务完成!成功: %d, 失败: %d", success.get(), failed.get()), true);
} catch (Exception e) {
log.error("转码任务执行失败", e);
sendProgress(taskId, 0, 0, 0, 0, null,
"任务执行失败: " + e.getMessage(), true);
}
}
private boolean shouldConvert(Path file) {
String ext = getExtension(file.getFileName().toString());
if (ext == null) return false;
return LOSSLESS_EXTENSIONS.contains(ext);
}
private String getExtension(String fileName) {
int i = fileName.lastIndexOf('.');
if (i == -1 || i == fileName.length() - 1) return null;
return fileName.substring(i + 1).toLowerCase();
}
private String getBaseName(String fileName) {
int i = fileName.lastIndexOf('.');
if (i <= 0) return fileName;
return fileName.substring(0, i);
}
private void runFfmpeg(Path input, Path output) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-y",
"-i", input.toAbsolutePath().toString(),
"-compression_level", String.valueOf(FFMPEG_COMPRESSION_LEVEL),
output.toAbsolutePath().toString()
);
pb.redirectErrorStream(true);
Process p = pb.start();
int exit = p.waitFor();
if (exit != 0) {
throw new RuntimeException("ffmpeg 退出码: " + exit);
}
}
private Path resolveTargetFile(Path targetDir, String fileName) throws IOException {
Path target = targetDir.resolve(fileName);
if (!Files.exists(target)) return target;
int lastDot = fileName.lastIndexOf('.');
String base = lastDot > 0 ? fileName.substring(0, lastDot) : fileName;
String ext = lastDot > 0 ? fileName.substring(lastDot) : "";
int n = 1;
while (Files.exists(target)) {
String next = base + " (" + n + ")" + ext;
target = targetDir.resolve(next);
n++;
}
return target;
}
private void sendProgress(String taskId, int total, int processed, int success,
int failed, String currentFile, String message, boolean completed) {
try {
ProgressMessage pm = new ProgressMessage();
pm.setTaskId(taskId);
pm.setType("convert");
pm.setTotal(total);
pm.setProcessed(processed);
pm.setSuccess(success);
pm.setFailed(failed);
pm.setCurrentFile(currentFile);
pm.setMessage(message);
pm.setCompleted(completed);
progressStore.put(pm);
messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm);
log.debug("发送进度消息: taskId={}, total={}, processed={}, success={}, failed={}, completed={}",
taskId, total, processed, success, failed, completed);
} catch (Exception e) {
log.error("发送进度消息失败", e);
}
}
}