diff --git a/src/main/java/com/lion/lionwebsite/Util/GalleryPageParser.java b/src/main/java/com/lion/lionwebsite/Util/GalleryPageParser.java
index e3ae48b..1dda616 100644
--- a/src/main/java/com/lion/lionwebsite/Util/GalleryPageParser.java
+++ b/src/main/java/com/lion/lionwebsite/Util/GalleryPageParser.java
@@ -11,6 +11,10 @@ import java.util.regex.Pattern;
public final class GalleryPageParser {
private GalleryPageParser() { }
+ /** 归档页 #db 单元格开头:分辨率标签 + 正数体积 + 受支持单位;后缀费用文本(Free、50GP)忽略。 */
+ private static final Pattern RESOLUTION_ENTRY =
+ Pattern.compile("^(Original|(\\d+)x)\\s+(\\d+(?:\\.\\d+)?)\\s*([A-Za-z]+)(?=\\s|$)", Pattern.CASE_INSENSITIVE);
+
public static Gallery metadata(Document document, String link, int gid) throws IOException {
String title = document.select("#gn").text();
if (title.isBlank()) title = document.select("#gj").text();
@@ -52,16 +56,37 @@ public final class GalleryPageParser {
throw new IOException("上游页面缺少归档入口");
}
+ /**
+ * 解析归档页 #db 的可选分辨率。每个单元格合并后的文本形如 {@code Original 36.31 MiB Free}、
+ * {@code 800x 5.65 MiB Free}、{@code 2560x N/A N/A},单段布局形如 {@code 1280x 12.5 MiB},
+ * 都由同一条锚定规则覆盖;只看开头(分辨率 + 正数体积 + 单位),故 N/A 与费用文本不匹配,
+ * 配对也不会跨 {@code
} 借用相邻格。体积须能被 {@link CustomUtil#stringToFileSize(String)}
+ * 解析为正数,宽度须为正且不溢出。结果按宽度升序、{@code Original} 最后;无可用项抛 IOException。
+ */
public static Map resolutions(Document document) throws IOException {
- var expression = Pattern.compile("^(\\d+x|Original)\\s+([\\d.]+)\\s*([A-Za-z]+)$", Pattern.CASE_INSENSITIVE);
TreeMap sizes = new TreeMap<>();
String original = null;
- for (Element paragraph : document.select("#db td p")) {
- var matcher = expression.matcher(paragraph.text().strip());
- if (!matcher.matches()) continue;
- String size = matcher.group(2) + matcher.group(3);
- if (matcher.group(1).equalsIgnoreCase("Original")) original = size;
- else sizes.put(Integer.parseInt(matcher.group(1).replace("x", "")), size);
+ for (Element cell : document.select("#db td")) {
+ var matcher = RESOLUTION_ENTRY.matcher(cell.text().replace('\u00a0', ' ').strip());
+ if (!matcher.find()) continue;
+ String size = matcher.group(3) + matcher.group(4);
+ try {
+ if (CustomUtil.stringToFileSize(size) <= 0) continue;
+ } catch (RuntimeException unsupported) {
+ continue;
+ }
+ if (matcher.group(1).equalsIgnoreCase("Original")) {
+ original = size;
+ continue;
+ }
+ int width;
+ try {
+ width = Integer.parseInt(matcher.group(2));
+ } catch (NumberFormatException outOfRange) {
+ continue;
+ }
+ if (width <= 0) continue;
+ sizes.put(width, size);
}
Map result = new LinkedHashMap<>();
sizes.forEach((width, size) -> result.put(width + "x", size));
diff --git a/src/test/java/com/lion/lionwebsite/Util/UpstreamParserVariantsTest.java b/src/test/java/com/lion/lionwebsite/Util/UpstreamParserVariantsTest.java
index 9f9ebe9..a31d30f 100644
--- a/src/test/java/com/lion/lionwebsite/Util/UpstreamParserVariantsTest.java
+++ b/src/test/java/com/lion/lionwebsite/Util/UpstreamParserVariantsTest.java
@@ -3,6 +3,9 @@ package com.lion.lionwebsite.Util;
import org.jsoup.Jsoup;
import org.junit.jupiter.api.Test;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
class UpstreamParserVariantsTest {
@@ -33,4 +36,119 @@ class UpstreamParserVariantsTest {
assertEquals("value", JavaScriptAssignments.string("let mpvkey = 'value';", "mpvkey"));
assertThrows(IOException.class, () -> JavaScriptAssignments.array("imagelist = [{\"k\":\"x\"}", "imagelist"));
}
+
+ /** 线上真实结构:每个单元格拆成三个 (分辨率/体积/费用)。旧实现逐段匹配导致全部漏掉。 */
+ @Test void resolutionsReadSplitParagraphCellsFromLiveLayout() throws Exception {
+ var document = Jsoup.parse(" "
+ + "Original 36.31 MiB Free | "
+ + "800x 5.65 MiB Free | "
+ + "1280x 12.5 MiB Free | "
+ + "1920x 30.2 MiB Free | "
+ + "2560x N/A N/A | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("800x", "1280x", "1920x", "Original"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("5.65MiB", resolutions.get("800x"));
+ assertEquals("12.5MiB", resolutions.get("1280x"));
+ assertEquals("30.2MiB", resolutions.get("1920x"));
+ assertEquals("36.31MiB", resolutions.get("Original"), "Original 应排在最后");
+ assertFalse(resolutions.containsKey("2560x"), "N/A 的体积不得进结果");
+ }
+
+ /** 历史/单段布局:分辨率与体积在同一段 也必须继续兼容。 */
+ @Test void resolutionsKeepMergedParagraphLayoutCompatible() throws Exception {
+ var document = Jsoup.parse(" "
+ + "1280x 12.5 MiB | "
+ + "Original 30.2 MiB | "
+ + " ");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("1280x", "Original"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("12.5MiB", resolutions.get("1280x"));
+ assertEquals("30.2MiB", resolutions.get("Original"));
+ }
+
+ /** N/A 与体积分处两格时不得跨 td 拼接。 */
+ @Test void resolutionsNeverStitchAcrossCells() throws Exception {
+ var document = Jsoup.parse(""
+ + "2560x N/A N/A | "
+ + "1920x 30.2 MiB | "
+ + "800x N/A | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(Set.of("1920x"), resolutions.keySet());
+ assertEquals("30.2MiB", resolutions.get("1920x"));
+ assertTrue(resolutions.values().stream().noneMatch(size -> size.toUpperCase().contains("N/A")));
+ }
+
+ /** 单元格开头不是“分辨率标签 + 体积”时不得凭空造出条目。 */
+ @Test void resolutionsRequireResolutionLabelWithinSameCell() throws Exception {
+ var document = Jsoup.parse(""
+ + "800x 5.65 MiB | "
+ + "12.5 MiB | "
+ + "1920x Costs 50 GP | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("800x"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("5.65MiB", resolutions.get("800x"));
+ }
+
+ /** 嵌套标签(strong/span/div)与 由 text() 归并后仍应正确取到分辨率与体积。 */
+ @Test void resolutionsReadNestedTagsInsideCells() throws Exception {
+ var document = Jsoup.parse(""
+ + " | "
+ + "Original 30.2 MiB | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("1280x", "Original"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("12.5MiB", resolutions.get("1280x"));
+ assertEquals("30.2MiB", resolutions.get("Original"), " 分隔的体积也要认");
+ }
+
+ /** 未知单位、坏数字和费用文本都不得当作体积,只保留可解析的项。 */
+ @Test void resolutionsRejectUnknownUnitAndCostText() throws Exception {
+ var document = Jsoup.parse(""
+ + "1280x 12.5 ZiB | "
+ + "1920x Costs 50 GP | "
+ + "2560x .5 MiB | "
+ + "800x 5.65 MiB | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("800x"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("5.65MiB", resolutions.get("800x"));
+ }
+
+ /** 完全取不到可用分辨率(空文档或只有 N/A)必须抛 IOException,而不是返回空表。 */
+ @Test void resolutionsThrowWhenNothingUsable() throws Exception {
+ assertThrows(IOException.class, () -> GalleryPageParser.resolutions(Jsoup.parse("")));
+ assertThrows(IOException.class, () -> GalleryPageParser.resolutions(Jsoup.parse(
+ "")));
+ }
+
+ /** 宽度为 0 或溢出、以及单位后粘连字符的项都要跳过,不能崩到调用方。 */
+ @Test void resolutionsSkipInvalidWidthAndGluedUnit() throws Exception {
+ var document = Jsoup.parse(""
+ + "0x 5.65 MiB Free | "
+ + "99999999999x 12.5 MiB Free | "
+ + "800x 5.65 MiB2 Free | "
+ + "1280x 12.5 MiB Free | "
+ + "
");
+
+ var resolutions = GalleryPageParser.resolutions(document);
+
+ assertEquals(List.of("1280x"), new ArrayList<>(resolutions.keySet()));
+ assertEquals("12.5MiB", resolutions.get("1280x"));
+ }
}
|