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("
" + + "" + + "" + + "

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"), "  分隔的体积也要认"); + } + + /** 未知单位、坏数字和费用文本都不得当作体积,只保留可解析的项。 */ + @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( + "
" + + "

2560x

N/A

N/A

N/A

"))); + } + + /** 宽度为 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")); + } }