Template
feat: fetch and backfill cover art
This commit is contained in:
@@ -92,6 +92,25 @@ public class IngestService {
|
||||
private final AudioValidationService audioValidationService;
|
||||
@Autowired(required = false)
|
||||
private LyricsService lyricsService;
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
private CoverArtService coverArtService;
|
||||
private volatile CoverRun coverRun;
|
||||
|
||||
static final class CoverRun {
|
||||
final java.util.concurrent.atomic.AtomicInteger found = new java.util.concurrent.atomic.AtomicInteger();
|
||||
final java.util.concurrent.atomic.AtomicInteger missing = new java.util.concurrent.atomic.AtomicInteger();
|
||||
final java.util.concurrent.atomic.AtomicInteger failed = new java.util.concurrent.atomic.AtomicInteger();
|
||||
volatile String lastSource;
|
||||
volatile String lastStatus;
|
||||
void reset() { lastSource = null; lastStatus = null; }
|
||||
void record(String source, String status) {
|
||||
lastSource = source;
|
||||
lastStatus = status;
|
||||
if ("found".equals(status)) found.incrementAndGet();
|
||||
else if ("failed".equals(status)) failed.incrementAndGet();
|
||||
else missing.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
/** 任务生命周期持久化与取消(可选注入;测试构造器不设置时为 null,行为不变)。 */
|
||||
@Autowired(required = false)
|
||||
@@ -258,10 +277,12 @@ public class IngestService {
|
||||
AtomicInteger missingCover = new AtomicInteger(0);
|
||||
AtomicInteger processed = new AtomicInteger(0);
|
||||
|
||||
// 歌词统计收集器(歌词失败不影响入库,仅用于报告/进度观测)
|
||||
LyricRun run = new LyricRun();
|
||||
this.lyricRun = run;
|
||||
|
||||
CoverRun cr = new CoverRun();
|
||||
this.coverRun = cr;
|
||||
|
||||
// 登记任务生命周期(持久化,供重启恢复与取消)
|
||||
if (taskStore != null) {
|
||||
taskStore.begin(taskId, total);
|
||||
@@ -281,10 +302,10 @@ public class IngestService {
|
||||
break;
|
||||
}
|
||||
String fileName = srcFile.getFileName().toString();
|
||||
// 使用相对于 Input 的路径作为唯一跟踪键(防止不同子目录中的同名文件互相覆盖)
|
||||
String relativeKey = inputPath.relativize(srcFile).toString();
|
||||
String outcome = "unknown";
|
||||
run.reset();
|
||||
if (coverRun != null) coverRun.reset();
|
||||
try {
|
||||
outcome = processSingleFile(srcFile, libraryPath, rejectedPath,
|
||||
libraryIdentities, batchIdentities,
|
||||
@@ -296,14 +317,16 @@ public class IngestService {
|
||||
outcome = "rejected:exception";
|
||||
log.warn("处理文件异常: {} - {}", relativeKey, e.getMessage());
|
||||
}
|
||||
fileOutcomes.put(relativeKey, new FileReport(outcome, run.lastSource, run.lastStatus));
|
||||
fileOutcomes.put(relativeKey, new FileReport(outcome, run.lastSource, run.lastStatus, cr.lastSource, cr.lastStatus));
|
||||
if (taskStore != null) {
|
||||
taskStore.recordProcessed(taskId, relativeKey, outcome, run.lastSource, run.lastStatus,
|
||||
cr.lastSource, cr.lastStatus,
|
||||
new IngestTaskStore.Counters(
|
||||
ingested.get(), duplicates.get(), missingMeta.get(),
|
||||
unreadable.get(), convFailed.get(), otherRejected.get(),
|
||||
missingCover.get(),
|
||||
run.found.get(), run.missing.get(), run.failed.get()));
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get()));
|
||||
}
|
||||
|
||||
int p = processed.incrementAndGet();
|
||||
@@ -311,6 +334,7 @@ public class IngestService {
|
||||
missingMeta.get(), unreadable.get(), convFailed.get(),
|
||||
otherRejected.get(),
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get(),
|
||||
relativeKey,
|
||||
String.format("已处理 (%d/%d): %s", p, total, relativeKey),
|
||||
false);
|
||||
@@ -320,7 +344,8 @@ public class IngestService {
|
||||
writeReport(taskId, fileOutcomes, rejectedPath, ingested.get(), duplicates.get(),
|
||||
missingMeta.get(), unreadable.get(), convFailed.get(), otherRejected.get(),
|
||||
missingCover.get(), cleanupRemoved,
|
||||
run.found.get(), run.missing.get(), run.failed.get());
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get());
|
||||
|
||||
if (cancelled) {
|
||||
if (taskStore != null) {
|
||||
@@ -329,7 +354,8 @@ public class IngestService {
|
||||
sendProgress(taskId, total, processed.get(), ingested.get(), duplicates.get(),
|
||||
missingMeta.get(), unreadable.get(), convFailed.get(),
|
||||
otherRejected.get(),
|
||||
run.found.get(), run.missing.get(), run.failed.get(), null,
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get(), null,
|
||||
String.format("任务已取消:已处理 %d/%d,已入库 %d(已完成文件保持不动)",
|
||||
processed.get(), total, ingested.get()),
|
||||
true);
|
||||
@@ -343,11 +369,13 @@ public class IngestService {
|
||||
sendProgress(taskId, total, processed.get(), ingested.get(), duplicates.get(),
|
||||
missingMeta.get(), unreadable.get(), convFailed.get(),
|
||||
otherRejected.get(),
|
||||
run.found.get(), run.missing.get(), run.failed.get(), null,
|
||||
String.format("导入完成!成功: %d, 重复: %d, 缺元数据: %d, 缺封面: %d, 不可读: %d, 转码失败: %d, 其他: %d, 清理: %d, 歌词(有/无/失败): %d/%d/%d",
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get(), null,
|
||||
String.format("导入完成!成功: %d, 重复: %d, 缺元数据: %d, 缺封面: %d, 不可读: %d, 转码失败: %d, 其他: %d, 清理: %d, 歌词(有/无/失败): %d/%d/%d, 封面(有/无/失败): %d/%d/%d",
|
||||
ingested.get(), duplicates.get(), missingMeta.get(), missingCover.get(),
|
||||
unreadable.get(), convFailed.get(), otherRejected.get(), cleanupRemoved,
|
||||
run.found.get(), run.missing.get(), run.failed.get()),
|
||||
run.found.get(), run.missing.get(), run.failed.get(),
|
||||
cr.found.get(), cr.missing.get(), cr.failed.get()),
|
||||
true);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -361,6 +389,7 @@ public class IngestService {
|
||||
null, "任务内部错误: " + e.getMessage(), true);
|
||||
} finally {
|
||||
this.lyricRun = null;
|
||||
coverRun = null;
|
||||
if (runningLock != null) {
|
||||
runningLock.set(false);
|
||||
}
|
||||
@@ -695,10 +724,40 @@ public class IngestService {
|
||||
// (后备 remux 使用 -map 0:a:0 会丢弃 attached picture,故须从原始 srcFile 提取)。
|
||||
boolean albumAlreadyCovered = hasExistingCover(targetDir);
|
||||
Path writtenCover = null;
|
||||
|
||||
String coverSource = "none";
|
||||
String coverStatus = "missing";
|
||||
|
||||
if (!albumAlreadyCovered) {
|
||||
writtenCover = acquireCover(srcFile, tag, targetDir);
|
||||
|
||||
if (writtenCover != null) {
|
||||
coverSource = "embedded";
|
||||
coverStatus = "found";
|
||||
} else if (coverArtService != null) {
|
||||
CoverArtService.RemoteOutcome ro = coverArtService.resolveRemote(targetDir, effectiveAlbum, effectiveArtist, year);
|
||||
if (ro == CoverArtService.RemoteOutcome.FETCHED) {
|
||||
Path existing = findExistingCover(targetDir);
|
||||
try {
|
||||
if (existing != null && Files.size(existing) > 0) {
|
||||
writtenCover = existing;
|
||||
coverSource = "remote";
|
||||
coverStatus = "found";
|
||||
} else {
|
||||
writtenCover = null;
|
||||
coverStatus = "failed";
|
||||
}
|
||||
} catch (IOException e) {
|
||||
writtenCover = null;
|
||||
coverStatus = "failed";
|
||||
}
|
||||
} else if (ro == CoverArtService.RemoteOutcome.FAILED) {
|
||||
coverStatus = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
if (writtenCover == null) {
|
||||
// 无既有封面且无法提取内嵌封面 → MissingCover;清理派生临时文件,隔离源文件
|
||||
if (coverRun != null) coverRun.record(coverSource, coverStatus);
|
||||
if (derivedFile) {
|
||||
deleteIfExists(effectiveFile);
|
||||
}
|
||||
@@ -707,8 +766,13 @@ public class IngestService {
|
||||
log.info("缺少封面(无既有 cover 且无内嵌封面): {}", fileName);
|
||||
return "rejected:missing-cover";
|
||||
}
|
||||
} else {
|
||||
coverSource = "existing";
|
||||
coverStatus = "found";
|
||||
}
|
||||
|
||||
if (coverRun != null) coverRun.record(coverSource, coverStatus);
|
||||
|
||||
// 移动/复制到目标位置
|
||||
try {
|
||||
FileTransferUtils.moveWithFallback(effectiveFile, targetFile);
|
||||
@@ -943,11 +1007,15 @@ public class IngestService {
|
||||
final String outcome;
|
||||
final String lyricSource;
|
||||
final String lyricStatus;
|
||||
final String coverSource;
|
||||
final String coverStatus;
|
||||
|
||||
FileReport(String outcome, String lyricSource, String lyricStatus) {
|
||||
FileReport(String outcome, String lyricSource, String lyricStatus, String coverSource, String coverStatus) {
|
||||
this.outcome = outcome;
|
||||
this.lyricSource = lyricSource;
|
||||
this.lyricStatus = lyricStatus;
|
||||
this.coverSource = coverSource;
|
||||
this.coverStatus = coverStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +1024,8 @@ public class IngestService {
|
||||
int missingMetaCount, int unreadableCount,
|
||||
int convFailedCount, int otherRejectedCount,
|
||||
int missingCoverCount, int cleanupRemovedCount,
|
||||
int lyricsFoundCount, int lyricsMissingCount, int lyricsFailedCount) {
|
||||
int lyricsFoundCount, int lyricsMissingCount, int lyricsFailedCount,
|
||||
int coversFoundCount, int coversMissingCount, int coversFailedCount) {
|
||||
try {
|
||||
Path reportsDir = rejectedPath.resolve(REPORT_SUBDIR);
|
||||
Files.createDirectories(reportsDir);
|
||||
@@ -982,7 +1051,10 @@ public class IngestService {
|
||||
json.append(" \"libraryCleanupRemoved\": ").append(cleanupRemovedCount).append(",\n");
|
||||
json.append(" \"lyricsFound\": ").append(lyricsFoundCount).append(",\n");
|
||||
json.append(" \"lyricsMissing\": ").append(lyricsMissingCount).append(",\n");
|
||||
json.append(" \"lyricsFailed\": ").append(lyricsFailedCount).append("\n");
|
||||
json.append(" \"lyricsFailed\": ").append(lyricsFailedCount).append(",\n");
|
||||
json.append(" \"coversFound\": ").append(coversFoundCount).append(",\n");
|
||||
json.append(" \"coversMissing\": ").append(coversMissingCount).append(",\n");
|
||||
json.append(" \"coversFailed\": ").append(coversFailedCount).append("\n");
|
||||
json.append(" },\n");
|
||||
json.append(" \"files\": [\n");
|
||||
|
||||
@@ -997,6 +1069,10 @@ public class IngestService {
|
||||
json.append(", \"lyricStatus\": \"").append(escapeJson(fr.lyricStatus)).append("\"");
|
||||
json.append(", \"lyricSource\": \"").append(escapeJson(fr.lyricSource)).append("\"");
|
||||
}
|
||||
if (fr.coverStatus != null) {
|
||||
json.append(", \"coverStatus\": \"").append(escapeJson(fr.coverStatus)).append("\"");
|
||||
json.append(", \"coverSource\": \"").append(escapeJson(fr.coverSource)).append("\"");
|
||||
}
|
||||
json.append("}");
|
||||
if (++idx < size) json.append(",");
|
||||
json.append("\n");
|
||||
@@ -1611,7 +1687,7 @@ public class IngestService {
|
||||
String currentFile, String message, boolean completed) {
|
||||
sendProgress(taskId, total, processed, ingestedFiles, duplicateFiles,
|
||||
missingMetadataFiles, unreadableFiles, conversionFailedFiles, otherRejectedFiles,
|
||||
null, null, null, currentFile, message, completed);
|
||||
null, null, null, null, null, null, currentFile, message, completed);
|
||||
}
|
||||
|
||||
private void sendProgress(String taskId, int total, int processed,
|
||||
@@ -1619,6 +1695,7 @@ public class IngestService {
|
||||
int missingMetadataFiles, int unreadableFiles,
|
||||
int conversionFailedFiles, int otherRejectedFiles,
|
||||
Integer lyricsFound, Integer lyricsMissing, Integer lyricsFailed,
|
||||
Integer coversFound, Integer coversMissing, Integer coversFailed,
|
||||
String currentFile, String message, boolean completed) {
|
||||
try {
|
||||
ProgressMessage pm = new ProgressMessage();
|
||||
@@ -1637,6 +1714,9 @@ public class IngestService {
|
||||
pm.setLyricsFound(lyricsFound);
|
||||
pm.setLyricsMissing(lyricsMissing);
|
||||
pm.setLyricsFailed(lyricsFailed);
|
||||
pm.setCoversFound(coversFound);
|
||||
pm.setCoversMissing(coversMissing);
|
||||
pm.setCoversFailed(coversFailed);
|
||||
pm.setCurrentFile(currentFile);
|
||||
pm.setMessage(message);
|
||||
pm.setCompleted(completed);
|
||||
|
||||
Reference in New Issue
Block a user