提交代码
This commit is contained in:
219
backend/src/main/java/com/music/service/ConvertService.java
Normal file
219
backend/src/main/java/com/music/service/ConvertService.java
Normal file
@@ -0,0 +1,219 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user