提交代码

This commit is contained in:
liu
2026-01-30 00:04:31 +08:00
parent 7531b6c466
commit 89be3ba0bd
23 changed files with 4934 additions and 179 deletions

View File

@@ -0,0 +1,350 @@
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 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<String> AUDIO_EXTENSIONS = new HashSet<>(Arrays.asList(
"mp3", "flac", "wav", "m4a", "aac", "ogg", "wma", "ape", "aiff", "aif", "wv", "tta", "opus"
));
private static final Set<String> LYRICS_EXTENSIONS = new HashSet<>(Arrays.asList("lrc"));
private static final Set<String> COVER_NAMES = new HashSet<>(Arrays.asList("cover.jpg", "cover.png", "folder.jpg", "folder.png"));
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, null, "源目录不能为空", true);
return;
}
if (!Files.exists(srcPath) || !Files.isDirectory(srcPath)) {
sendProgress(taskId, 0, 0, 0, 0, 0, null, "源目录不存在或不是目录", true);
return;
}
if (dstDir == null || dstDir.trim().isEmpty()) {
sendProgress(taskId, 0, 0, 0, 0, 0, null, "目标目录不能为空", true);
return;
}
if (srcPath.normalize().equals(dstPath.normalize())) {
sendProgress(taskId, 0, 0, 0, 0, 0, null, "源目录与目标目录不能相同", true);
return;
}
if (!Files.exists(dstPath)) {
Files.createDirectories(dstPath);
}
// 收集源目录中的所有文件(音频、歌词、封面)
List<Path> audioFiles = new ArrayList<>();
Map<Path, Path> lyricsMap = new HashMap<>(); // 音频文件 -> 歌词文件
Map<Path, Path> coverMap = new HashMap<>(); // 专辑目录 -> 封面文件
Files.walkFileTree(srcPath, new SimpleFileVisitor<Path>() {
@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, 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<String> processedAlbums = new HashSet<>();
sendProgress(taskId, total, 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);
}
Files.copy(audioFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
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(), 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(), audioFile.getFileName().toString(),
String.format("已处理 %d/%d (升级: %d)", p, total, upgraded.get()), false);
}
continue;
}
} else {
// 新文件,直接复制
Files.createDirectories(targetPath.getParent());
Files.copy(audioFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
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) {
Files.copy(lyricsFile, lyricsTarget, StandardCopyOption.REPLACE_EXISTING);
}
}
// 处理封面(每个专辑目录只处理一次)
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)) {
// 目标目录没有封面,直接复制
Files.copy(coverFile, coverTarget, StandardCopyOption.REPLACE_EXISTING);
} else {
// 比较封面,保留更好的版本
if (isBetterCover(coverFile, coverTarget)) {
if (keepBackup) {
Path backupPath = coverTarget.resolveSibling("cover.jpg.backup");
Files.copy(coverTarget, backupPath, StandardCopyOption.REPLACE_EXISTING);
}
Files.copy(coverFile, coverTarget, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
} 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(), audioFile.getFileName().toString(),
String.format("已处理 %d/%d (升级: %d)", p, total, upgraded.get()), false);
}
}
sendProgress(taskId, total, processed.get(), albumsMerged.get(), tracksMerged.get(),
upgraded.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, 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 void sendProgress(String taskId, int total, int processed, int albumsMerged,
int tracksMerged, int upgraded, 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.setCurrentFile(currentFile);
pm.setMessage(message);
pm.setCompleted(completed);
progressStore.put(pm);
messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm);
} catch (Exception e) {
log.error("发送进度失败", e);
}
}
}