Fix M4A fallback ingestion

This commit is contained in:
2026-07-19 23:29:55 +08:00
parent 4327b94fd0
commit 4c68e415ee
12 changed files with 1224 additions and 126 deletions
@@ -10,6 +10,7 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -436,6 +437,115 @@ class AudioValidationServiceTest {
}
}
// ========== FFprobe 后备元数据读取 ==========
@Test
void getMetadataCommandArgs_usesStructuredJson() {
Path f = Paths.get("/tmp/song.m4a");
List<String> args = service.getMetadataCommandArgs(f);
assertTrue(args.contains("-print_format"));
assertTrue(args.contains("json"));
assertTrue(args.contains("-show_format"));
assertTrue(args.contains("-show_streams"));
// 绝不使用任何人类可读文本或文件名猜测选项
assertFalse(args.contains("-of"));
assertEquals(f.toAbsolutePath().toString(), args.get(args.size() - 1));
}
@Test
void readMetadata_returnsFullTags() throws Exception {
assumeFfmpegFfprobe();
Path tmpDir = Files.createTempDirectory("probe-meta-");
try {
Path m4a = createTaggedM4a(tmpDir, "song.m4a",
"ProbeTitle", "ProbeArtist", "ProbeAlbum");
AudioValidationService.ProbeMetadata meta = service.readMetadata(m4a);
assertTrue(meta.isAvailable(), "应成功读取元数据");
assertEquals("ProbeTitle", meta.getFirst("title"));
assertEquals("ProbeArtist", meta.getFirst("artist"));
assertEquals("ProbeAlbum", meta.getFirst("album"));
} finally {
deleteDirectory(tmpDir);
}
}
@Test
void readMetadata_missingAlbumReturnsEmpty() throws Exception {
assumeFfmpegFfprobe();
Path tmpDir = Files.createTempDirectory("probe-meta-");
try {
// 仅写 title/artist,故意不写 album
Path m4a = tmpDir.resolve("noalbum.m4a");
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-y", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono",
"-t", "0.3", "-c:a", "aac",
"-metadata", "title=T", "-metadata", "artist=A",
m4a.toAbsolutePath().toString());
pb.redirectErrorStream(true);
Process p = pb.start();
readAllBytes(p.getInputStream());
p.waitFor(15, TimeUnit.SECONDS);
AudioValidationService.ProbeMetadata meta = service.readMetadata(m4a);
assertTrue(meta.isAvailable());
assertEquals("", meta.getFirst("album"), "缺失的 album 应返回空字符串");
} finally {
deleteDirectory(tmpDir);
}
}
@Test
void readMetadata_nonAudioFileUnavailable() throws Exception {
assumeFfmpegFfprobe();
Path tmpDir = Files.createTempDirectory("probe-meta-");
try {
Path bogus = tmpDir.resolve("bogus.m4a");
Files.write(bogus, "not an audio file".getBytes(StandardCharsets.UTF_8));
AudioValidationService.ProbeMetadata meta = service.readMetadata(bogus);
assertFalse(meta.isAvailable(), "无法识别的文件应返回 unavailable");
} finally {
deleteDirectory(tmpDir);
}
}
@Test
void readMetadata_ffprobeFailureUnavailable() {
// 强制 ffprobe 指向失败命令
System.setProperty(AudioValidationService.FFPROBE_BIN_PROPERTY, "/bin/false");
AudioValidationService.ProbeMetadata meta =
service.readMetadata(Paths.get("/tmp/whatever.m4a"));
assertFalse(meta.isAvailable(), "ffprobe 失败应返回 unavailable");
}
/**
* 使用 FFmpeg 创建一个带标签的有效短 M4A(AAC)文件。
*/
private static Path createTaggedM4a(Path dir, String fileName,
String title, String artist, String album) throws Exception {
Path out = dir.resolve(fileName);
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-y", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono",
"-t", "0.3", "-c:a", "aac",
"-metadata", "title=" + title,
"-metadata", "artist=" + artist,
"-metadata", "album=" + album,
out.toAbsolutePath().toString());
pb.redirectErrorStream(true);
Process p = pb.start();
readAllBytes(p.getInputStream());
boolean finished = p.waitFor(15, TimeUnit.SECONDS);
if (!finished) {
p.destroyForcibly();
throw new RuntimeException("ffmpeg 创建 M4A 超时");
}
if (p.exitValue() != 0) {
throw new RuntimeException("ffmpeg 创建 M4A 失败 (exit=" + p.exitValue() + ")");
}
return out;
}
// ========== 帮助方法 ==========
/**