220 lines
8.7 KiB
Java
220 lines
8.7 KiB
Java
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);
|
||
}
|
||
}
|
||
}
|