104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
"""分类页/搜索页 __INITIAL_STATE__ → GenreData
|
|
|
|
分类数据来自 state.data.genreTree.parent_category。它不是完整分类树,而是一条
|
|
「从根一路展开到目标分类」的链:每层只保留通往目标的那一个子节点,目标分类自身
|
|
则挂着它的全部直接子分类。
|
|
|
|
不带分类查询时,根节点直接挂着 39 个顶层分类。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.core import site
|
|
from app.core.errors import ScrapeParseError
|
|
from app.models.scrape import GenreData, GenreNode
|
|
from app.utils.coerce import as_dict, as_int, as_list, as_str
|
|
|
|
# 站点用 id=0 表示分类树的虚拟根,它不是一个真实分类
|
|
ROOT_GENRE_ID = 0
|
|
|
|
|
|
def _genre_url(genre_id: str) -> str:
|
|
return f"{site.CATEGORY_BASE_URL}{genre_id}/" if genre_id else ""
|
|
|
|
|
|
def _to_node(raw: dict[str, Any], *, with_count: bool) -> GenreNode:
|
|
genre_id = str(as_int(raw.get("id")))
|
|
count = raw.get("count")
|
|
return GenreNode(
|
|
genre_id=genre_id,
|
|
name=as_str(raw.get("name")),
|
|
item_count=as_int(count) if with_count and count is not None else None,
|
|
shortcut=as_str(raw.get("shortcut")),
|
|
is_leaf=bool(raw.get("leaf")),
|
|
url=_genre_url(genre_id),
|
|
)
|
|
|
|
|
|
def _find_path(node: dict[str, Any], genre_id: str) -> list[dict[str, Any]] | None:
|
|
"""在分类链中定位目标分类,返回从根到它的节点路径(含自身)"""
|
|
if str(as_int(node.get("id"))) == genre_id:
|
|
return [node]
|
|
for child in as_list(node.get("children")):
|
|
if not isinstance(child, dict):
|
|
continue
|
|
found = _find_path(child, genre_id)
|
|
if found is not None:
|
|
return [node, *found]
|
|
return None
|
|
|
|
|
|
def parse_genres(state: dict[str, Any], *, genre_id: str | None) -> GenreData:
|
|
"""解析分类树
|
|
|
|
Args:
|
|
genre_id: 目标分类;None 表示取顶层分类列表
|
|
|
|
Raises:
|
|
ScrapeParseError: 页面里没有分类树,或目标分类不在返回的链上
|
|
"""
|
|
data = as_dict(as_dict(state.get("state")).get("data"))
|
|
root = as_dict(as_dict(data.get("genreTree")).get("parent_category"))
|
|
if not root:
|
|
raise ScrapeParseError("页面中缺少 genreTree.parent_category 节点")
|
|
|
|
if genre_id is None:
|
|
children = [
|
|
_to_node(child, with_count=False)
|
|
for child in as_list(root.get("children"))
|
|
if isinstance(child, dict)
|
|
]
|
|
if not children:
|
|
raise ScrapeParseError("未能取到顶层分类列表")
|
|
return GenreData(children=children)
|
|
|
|
path = _find_path(root, genre_id)
|
|
if path is None:
|
|
raise ScrapeParseError(f"分类树中未找到分类 {genre_id}")
|
|
|
|
target = path[-1]
|
|
ancestors = [
|
|
_to_node(node, with_count=False)
|
|
for node in path[:-1]
|
|
if as_int(node.get("id")) != ROOT_GENRE_ID
|
|
]
|
|
children = [
|
|
_to_node(child, with_count=True)
|
|
for child in as_list(target.get("children"))
|
|
if isinstance(child, dict)
|
|
]
|
|
|
|
genre_info = as_dict(data.get("genreInfo"))
|
|
resolved_id = str(as_int(target.get("id")))
|
|
return GenreData(
|
|
genre_id=resolved_id,
|
|
name=as_str(target.get("name")),
|
|
full_name=as_str(genre_info.get("fullGenreName")),
|
|
description=as_str(genre_info.get("description")),
|
|
is_leaf=bool(target.get("leaf")),
|
|
url=_genre_url(resolved_id),
|
|
ancestors=ancestors,
|
|
children=children,
|
|
)
|