diff --git a/app/modules/indexer/parser/gazelle.py b/app/modules/indexer/parser/gazelle.py index b6833db11..7215bef59 100644 --- a/app/modules/indexer/parser/gazelle.py +++ b/app/modules/indexer/parser/gazelle.py @@ -140,17 +140,41 @@ class GazelleSiteUserInfo(SiteParserBase): page_seeding = 0 page_seeding_size = 0 page_seeding_info = [] - seeding_sizes = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{size_col}]') - seeding_seeders = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{seeders_col}]/text()') - if seeding_sizes and seeding_seeders: - page_seeding = len(seeding_sizes) - - for i in range(0, len(seeding_sizes)): - size = size_tools.parse_size(seeding_sizes[i].xpath("string(.)").strip()) - seeders = int(seeding_seeders[i]) - + semantic_rows = html.xpath( + '//table[contains(@id, "torrent")]//tr[' + 'contains(concat(" ", normalize-space(@class), " "), " TableTorrent-rowTitle ")]' + ) + if semantic_rows: + # GPW 等现代 Gazelle 页面以语义 class 标记列,且标题单元格带 colspan, + # 固定列序号无法反映视觉列位置,必须逐行按 class 配对读取。 + for row in semantic_rows: + size_cells = row.xpath( + './td[contains(concat(" ", normalize-space(@class), " "), ' + '" TableTorrent-cellStatSize ")]' + ) + seeder_cells = row.xpath( + './td[contains(concat(" ", normalize-space(@class), " "), ' + '" TableTorrent-cellStatSeeders ")]' + ) + if not size_cells or not seeder_cells: + continue + size = size_tools.parse_size(size_cells[0].xpath("string(.)").strip()) + seeders = text_tools.parse_int(seeder_cells[0].xpath("string(.)").strip()) + page_seeding += 1 page_seeding_size += size page_seeding_info.append([seeders, size]) + else: + seeding_sizes = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{size_col}]') + seeding_seeders = html.xpath(f'//table[contains(@id, "torrent")]//tr[position()>1]/td[{seeders_col}]/text()') + if seeding_sizes and seeding_seeders: + page_seeding = len(seeding_sizes) + + for i in range(0, len(seeding_sizes)): + size = size_tools.parse_size(seeding_sizes[i].xpath("string(.)").strip()) + seeders = text_tools.parse_int(seeding_seeders[i]) + + page_seeding_size += size + page_seeding_info.append([seeders, size]) if multi_page: self.seeding += page_seeding diff --git a/tests/test_greatposterwall_indexer.py b/tests/test_greatposterwall_indexer.py new file mode 100644 index 000000000..9efab6f07 --- /dev/null +++ b/tests/test_greatposterwall_indexer.py @@ -0,0 +1,140 @@ +from app.modules.indexer.parser.gazelle import GazelleSiteUserInfo +from app.modules.indexer.spider import SiteSpider + + +GPW_HTML = """ + + + + + + + + + + +
+ + + today + 1.50 GiB121,2342
+ +""" + + +def _gpw_indexer() -> dict: + """构造与站点资源一致的 GPW 搜索配置。""" + detail_selector = 'a[href*="torrents.php?id="][href*="torrentid="]' + return { + "id": "greatposterwall", + "name": "海豹", + "domain": "https://greatposterwall.com/", + "search": { + "paths": [ + {"path": "torrents.php?searchstr={keyword}", "method": "get"} + ] + }, + "torrents": { + "list": { + "selector": "table#torrent_table > tbody > tr.TableTorrent-rowTitle" + }, + "fields": { + "id": { + "selector": detail_selector, + "attribute": "href", + "filters": [ + {"name": "re_search", "args": [r"torrentid=(\d+)", 1]} + ], + }, + "title": {"selector": f"{detail_selector} > span.TorrentTitle"}, + "details": {"selector": detail_selector, "attribute": "href"}, + "download": { + "selector": 'a[href*="torrents.php?action=download"]', + "attribute": "href", + }, + "size": {"selector": "td.TableTorrent-cellStatSize"}, + "seeders": {"selector": "td.TableTorrent-cellStatSeeders"}, + "leechers": {"selector": "td.TableTorrent-cellStatLeechers"}, + "grabs": {"selector": "td.TableTorrent-cellStatSnatches"}, + "date_elapsed": { + "selector": "td.TableTorrent-cellStatTime > span", + "attribute": "title", + "optional": True, + }, + "date": { + "text": "{% if fields['date_elapsed'] %}{{ fields['date_elapsed'] }}{% else %}now{% endif %}", + "filters": [{"name": "dateparse", "args": "%b %d %Y, %H:%M"}], + }, + }, + }, + } + + +def _build_parser() -> GazelleSiteUserInfo: + """构造 Gazelle 用户数据解析器测试实例。""" + return GazelleSiteUserInfo( + site_name="海豹", + url="https://greatposterwall.com/", + site_cookie="", + apikey=None, + token=None, + ) + + +def test_greatposterwall_search_parses_tabletorrent_row(monkeypatch): + """GPW 搜索结果应从 TableTorrent 行提取完整种子信息。""" + monkeypatch.setattr( + "app.modules.indexer.spider.rust_accel.parse_indexer_torrents", + lambda **_: None, + ) + + results = SiteSpider(_gpw_indexer(), keyword="Example").parse(GPW_HTML) + + assert len(results) == 1 + assert results[0]["title"] == "1080p Example-GROUP" + assert results[0]["page_url"].endswith("torrents.php?id=8064&torrentid=115362") + assert results[0]["enclosure"].endswith("torrents.php?action=download&id=115362") + assert results[0]["size"] == 1610612736 + assert results[0]["seeders"] == 1234 + assert results[0]["peers"] == 2 + assert results[0]["grabs"] == 12 + assert results[0]["pubdate"] == "2026-08-24 22:51:00" + + +def test_greatposterwall_seeding_uses_semantic_stat_cells(): + """GPW 标题列使用 colspan 时,做种统计仍应按语义 class 正确配对。""" + parser = _build_parser() + + next_page = parser._parse_user_torrent_seeding_info(GPW_HTML) + + assert next_page is None + assert parser.seeding == 1 + assert parser.seeding_size == 1610612736 + assert parser.seeding_info == [[1234, 1610612736]] + + +def test_gazelle_seeding_keeps_legacy_column_fallback(): + """没有语义 class 的传统 Gazelle 表格仍应按既有列号统计做种。""" + parser = _build_parser() + html_text = """ + + + +
NameTypeTimeSizeSnatchesSeedersLeechers
ExampleMovietoday2 GiB3451
+ """ + + parser._parse_user_torrent_seeding_info(html_text) + + assert parser.seeding == 1 + assert parser.seeding_size == 2147483648 + assert parser.seeding_info == [[45, 2147483648]]