package com.music.service; import com.music.common.FileTransferUtils; 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 javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * 整理入库服务:将整理好的 staging 目录智能合并到 Navidrome 主库中 */ @Service public class LibraryMergeService { private static final Logger log = LoggerFactory.getLogger(LibraryMergeService.class); private static final Set AUDIO_EXTENSIONS = new HashSet<>(Arrays.asList( "mp3", "flac", "wav", "m4a", "aac", "ogg", "wma", "ape", "aiff", "aif", "wv", "tta", "opus" )); private static final Set LYRICS_EXTENSIONS = new HashSet<>(Arrays.asList("lrc")); private static final Set COVER_NAMES = new HashSet<>(Arrays.asList("cover.jpg", "cover.png", "folder.jpg", "folder.png")); private static final Set EXCLUDED_ROOT_DIRS = new HashSet<>(Arrays.asList( "_Manual_Fix_Required_", "_Reports" )); private final SimpMessagingTemplate messagingTemplate; private final ProgressStore progressStore; public LibraryMergeService(SimpMessagingTemplate messagingTemplate, ProgressStore progressStore) { this.messagingTemplate = messagingTemplate; this.progressStore = progressStore; } @Async public void merge(String taskId, String srcDir, String dstDir, boolean smartUpgrade, boolean keepBackup) { Path srcPath = Paths.get(srcDir); Path dstPath = Paths.get(dstDir); try { // 基本校验 if (srcDir == null || srcDir.trim().isEmpty()) { sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "源目录不能为空", true); return; } if (!Files.exists(srcPath) || !Files.isDirectory(srcPath)) { sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "源目录不存在或不是目录", true); return; } if (dstDir == null || dstDir.trim().isEmpty()) { sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "目标目录不能为空", true); return; } if (srcPath.normalize().equals(dstPath.normalize())) { sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "源目录与目标目录不能相同", true); return; } if (!Files.exists(dstPath)) { Files.createDirectories(dstPath); } // 收集源目录中的所有文件(音频、歌词、封面) List audioFiles = new ArrayList<>(); Map lyricsMap = new HashMap<>(); // 音频文件 -> 歌词文件 Map coverMap = new HashMap<>(); // 专辑目录 -> 封面文件 Files.walkFileTree(srcPath, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (isExcludedSystemDirectory(srcPath, dir)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { String fileName = file.getFileName().toString().toLowerCase(); String ext = getExtension(fileName); if (isAudioFile(file)) { audioFiles.add(file); // 查找同目录下的歌词文件 Path lyrics = findLyricsFile(file); if (lyrics != null) { lyricsMap.put(file, lyrics); } } else if (isCoverFile(fileName)) { // 封面文件,关联到父目录 Path albumDir = file.getParent(); if (albumDir != null) { coverMap.put(albumDir, file); } } return FileVisitResult.CONTINUE; } }); int total = audioFiles.size(); if (total == 0) { sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "未在源目录中找到音频文件", true); return; } AtomicInteger processed = new AtomicInteger(0); AtomicInteger albumsMerged = new AtomicInteger(0); AtomicInteger tracksMerged = new AtomicInteger(0); AtomicInteger upgraded = new AtomicInteger(0); AtomicInteger skipped = new AtomicInteger(0); Set processedAlbums = new HashSet<>(); sendProgress(taskId, total, 0, 0, 0, 0, 0, null, "开始合并...", false); for (Path audioFile : audioFiles) { try { // 计算相对路径(相对于源目录) Path relativePath = srcPath.relativize(audioFile); Path targetPath = dstPath.resolve(relativePath); // 获取专辑目录(用于统计和封面处理) Path albumDir = targetPath.getParent(); String albumKey = albumDir != null ? albumDir.toString() : ""; boolean isNewAlbum = !processedAlbums.contains(albumKey); if (isNewAlbum) { processedAlbums.add(albumKey); albumsMerged.incrementAndGet(); } // 处理音频文件 boolean wasUpgraded = false; if (Files.exists(targetPath)) { // 目标文件已存在,需要判断是否升级 if (smartUpgrade) { long srcSize = Files.size(audioFile); long dstSize = Files.size(targetPath); if (srcSize > dstSize * 1.1) { // 新文件明显更大(10%阈值) // 备份旧文件(如果需要) if (keepBackup) { Path backupPath = targetPath.resolveSibling( targetPath.getFileName().toString() + ".backup"); Files.copy(targetPath, backupPath, StandardCopyOption.REPLACE_EXISTING); } FileTransferUtils.moveWithFallback(audioFile, targetPath); wasUpgraded = true; upgraded.incrementAndGet(); } else { skipped.incrementAndGet(); int p = processed.incrementAndGet(); if (p % 20 == 0 || p == total) { sendProgress(taskId, total, p, albumsMerged.get(), tracksMerged.get(), upgraded.get(), skipped.get(), audioFile.getFileName().toString(), String.format("已处理 %d/%d (升级: %d)", p, total, upgraded.get()), false); } continue; // 跳过,不升级 } } else { // 不启用智能升级,直接跳过已存在的文件 skipped.incrementAndGet(); int p = processed.incrementAndGet(); if (p % 20 == 0 || p == total) { sendProgress(taskId, total, p, albumsMerged.get(), tracksMerged.get(), upgraded.get(), skipped.get(), audioFile.getFileName().toString(), String.format("已处理 %d/%d (升级: %d)", p, total, upgraded.get()), false); } continue; } } else { // 新文件,直接移动 Files.createDirectories(targetPath.getParent()); FileTransferUtils.moveWithFallback(audioFile, targetPath); } tracksMerged.incrementAndGet(); // 同步关联的歌词文件 Path lyricsFile = lyricsMap.get(audioFile); if (lyricsFile != null && Files.exists(lyricsFile)) { Path lyricsTarget = targetPath.resolveSibling(lyricsFile.getFileName().toString()); if (!Files.exists(lyricsTarget) || wasUpgraded) { FileTransferUtils.moveWithFallback(lyricsFile, lyricsTarget); } } // 处理封面(每个专辑目录只处理一次) if (isNewAlbum && albumDir != null) { Path srcAlbumDir = audioFile.getParent(); Path coverFile = coverMap.get(srcAlbumDir); if (coverFile != null && Files.exists(coverFile)) { Path coverTarget = albumDir.resolve("cover.jpg"); if (!Files.exists(coverTarget)) { // 目标目录没有封面,直接移动 FileTransferUtils.moveWithFallback(coverFile, coverTarget); } else { // 比较封面,保留更好的版本 if (isBetterCover(coverFile, coverTarget)) { if (keepBackup) { Path backupPath = coverTarget.resolveSibling("cover.jpg.backup"); Files.copy(coverTarget, backupPath, StandardCopyOption.REPLACE_EXISTING); } FileTransferUtils.moveWithFallback(coverFile, coverTarget); } } } } } catch (Exception e) { log.warn("合并文件失败: {} - {}", audioFile, e.getMessage()); skipped.incrementAndGet(); } int p = processed.incrementAndGet(); if (p % 20 == 0 || p == total) { sendProgress(taskId, total, p, albumsMerged.get(), tracksMerged.get(), upgraded.get(), skipped.get(), audioFile.getFileName().toString(), String.format("已处理 %d/%d (升级: %d)", p, total, upgraded.get()), false); } } sendProgress(taskId, total, processed.get(), albumsMerged.get(), tracksMerged.get(), upgraded.get(), skipped.get(), null, String.format("合并完成!专辑: %d, 曲目: %d, 升级: %d, 跳过: %d", albumsMerged.get(), tracksMerged.get(), upgraded.get(), skipped.get()), true); } catch (Exception e) { log.error("合并任务执行失败", e); sendProgress(taskId, 0, 0, 0, 0, 0, 0, null, "任务执行失败: " + e.getMessage(), true); } } /** * 判断是否为音频文件 */ private boolean isAudioFile(Path file) { String ext = getExtension(file.getFileName().toString()); return ext != null && AUDIO_EXTENSIONS.contains(ext.toLowerCase()); } /** * 判断是否为歌词文件 */ private boolean isLyricsFile(String fileName) { String ext = getExtension(fileName); return ext != null && LYRICS_EXTENSIONS.contains(ext.toLowerCase()); } /** * 判断是否为封面文件 */ private boolean isCoverFile(String fileName) { return COVER_NAMES.contains(fileName.toLowerCase()); } /** * 查找音频文件对应的歌词文件 */ private Path findLyricsFile(Path audioFile) { Path parent = audioFile.getParent(); if (parent == null) { return null; } String baseName = getBaseName(audioFile.getFileName().toString()); for (String ext : LYRICS_EXTENSIONS) { Path lyricsPath = parent.resolve(baseName + "." + ext); if (Files.exists(lyricsPath)) { return lyricsPath; } } return null; } /** * 比较两个封面文件,判断新封面是否更好 * 策略:优先比较分辨率,其次比较文件大小 */ private boolean isBetterCover(Path newCover, Path existingCover) { try { BufferedImage newImg = ImageIO.read(newCover.toFile()); BufferedImage existingImg = ImageIO.read(existingCover.toFile()); if (newImg == null || existingImg == null) { // 无法读取图片,比较文件大小 return Files.size(newCover) > Files.size(existingCover); } int newPixels = newImg.getWidth() * newImg.getHeight(); int existingPixels = existingImg.getWidth() * existingImg.getHeight(); if (newPixels > existingPixels * 1.1) { // 新封面分辨率明显更高(10%阈值) return true; } else if (newPixels < existingPixels * 0.9) { // 新封面分辨率明显更低 return false; } else { // 分辨率相近,比较文件大小 return Files.size(newCover) > Files.size(existingCover); } } catch (IOException e) { log.debug("比较封面失败: {} vs {}", newCover, existingCover, e); // 比较失败,使用文件大小作为后备策略 try { return Files.size(newCover) > Files.size(existingCover); } catch (IOException ex) { return false; } } } /** * 获取文件扩展名 */ private String getExtension(String fileName) { int i = fileName.lastIndexOf('.'); return (i > 0 && i < fileName.length() - 1) ? fileName.substring(i + 1) : ""; } /** * 获取文件名(不含扩展名) */ private String getBaseName(String fileName) { int i = fileName.lastIndexOf('.'); return i > 0 ? fileName.substring(0, i) : fileName; } private boolean isExcludedSystemDirectory(Path sourceRoot, Path dir) { if (dir == null || sourceRoot == null) { return false; } if (dir.equals(sourceRoot)) { return false; } Path relative = sourceRoot.relativize(dir); if (relative.getNameCount() <= 0) { return false; } String firstSegment = relative.getName(0).toString(); return EXCLUDED_ROOT_DIRS.contains(firstSegment); } /** * 发送进度消息 */ private void sendProgress(String taskId, int total, int processed, int albumsMerged, int tracksMerged, int upgraded, int skipped, String currentFile, String message, boolean completed) { try { ProgressMessage pm = new ProgressMessage(); pm.setTaskId(taskId); pm.setType("merge"); pm.setTotal(total); pm.setProcessed(processed); pm.setSuccess(albumsMerged); // 使用 success 字段存储专辑数 pm.setFailed(tracksMerged); // 使用 failed 字段存储曲目数 pm.setAlbumsMerged(albumsMerged); pm.setTracksMerged(tracksMerged); pm.setUpgradedFiles(upgraded); pm.setSkippedFiles(skipped); pm.setCurrentFile(currentFile); pm.setMessage(message); pm.setCompleted(completed); progressStore.put(pm); messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm); } catch (Exception e) { log.error("发送进度失败", e); } } }