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 LOSSLESS_EXTENSIONS = new HashSet<>(Arrays.asList( "wav", "ape", "aiff", "aif", "wv", "tta" )); /** 有损格式及 FLAC:跳过,不处理 */ private static final Set 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 toConvert = new ArrayList<>(); Files.walkFileTree(srcPath, new SimpleFileVisitor() { @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); } } }