mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
fix: render indexer jinja fields in rust
This commit is contained in:
@@ -37,8 +37,9 @@ static NUMERIC_FACTOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+\.?\d*)").
|
|||||||
static FIELD_EXPR_RE: Lazy<Regex> = Lazy::new(|| {
|
static FIELD_EXPR_RE: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new(r#"^fields(?:\.([A-Za-z0-9_]+)|\[\s*['"]([^'"]+)['"]\s*\])$"#).unwrap()
|
Regex::new(r#"^fields(?:\.([A-Za-z0-9_]+)|\[\s*['"]([^'"]+)['"]\s*\])$"#).unwrap()
|
||||||
});
|
});
|
||||||
static JINJA_EXPR_RE: Lazy<Regex> =
|
static FIELD_REF_RE: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r#"\{\{-?\s*(.*?)\s*-?\}\}"#).unwrap());
|
Lazy::new(|| Regex::new(r#"fields(?:\.([A-Za-z0-9_]+)|\[\s*['"]([^'"]+)['"]\s*\])"#).unwrap());
|
||||||
|
static JINJA_EXPR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\{\{-?\s*(.*?)\s*-?\}\}"#).unwrap());
|
||||||
static JINJA_TAG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\{%-?\s*(.*?)\s*-?%\}"#).unwrap());
|
static JINJA_TAG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\{%-?\s*(.*?)\s*-?%\}"#).unwrap());
|
||||||
|
|
||||||
enum RowParseResult {
|
enum RowParseResult {
|
||||||
@@ -371,7 +372,7 @@ fn parse_indexer_row(
|
|||||||
Ok(RowParseResult::Item(output.into()))
|
Ok(RowParseResult::Item(output.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析标题字段,支持直接 selector 和常见的 title_default/title_optional 模板。
|
/// 解析标题字段,支持直接 selector 和按模板引用字段渲染 title.text。
|
||||||
fn parse_title(
|
fn parse_title(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
row: ElementRef<'_>,
|
row: ElementRef<'_>,
|
||||||
@@ -384,23 +385,12 @@ fn parse_title(
|
|||||||
let mut title = if selector.contains("selector")? {
|
let mut title = if selector.contains("selector")? {
|
||||||
safe_query(row, &selector)?
|
safe_query(row, &selector)?
|
||||||
} else if let Some(template) = get_optional_string(&selector, "text")? {
|
} else if let Some(template) = get_optional_string(&selector, "text")? {
|
||||||
let Some(default_selector) = get_field_dict(fields, "title_default")? else {
|
let values = collect_template_field_values(row, fields, &template)?;
|
||||||
return Ok(false);
|
let refs: Vec<(&str, &str)> = values
|
||||||
};
|
.iter()
|
||||||
let title_default = safe_query(row, &default_selector)?.unwrap_or_default();
|
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||||
let title_optional =
|
.collect();
|
||||||
if let Some(optional_selector) = get_field_dict(fields, "title_optional")? {
|
let Some(rendered) = render_known_template(&template, &refs) else {
|
||||||
safe_query(row, &optional_selector)?.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let Some(rendered) = render_known_template(
|
|
||||||
&template,
|
|
||||||
&[
|
|
||||||
("title_default", title_default.as_str()),
|
|
||||||
("title_optional", title_optional.as_str()),
|
|
||||||
],
|
|
||||||
) else {
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
Some(rendered)
|
Some(rendered)
|
||||||
@@ -414,7 +404,7 @@ fn parse_title(
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析描述字段,支持直接 selector 和常见 description 模板。
|
/// 解析描述字段,支持直接 selector 和按模板引用字段渲染 description.text。
|
||||||
fn parse_description(
|
fn parse_description(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
row: ElementRef<'_>,
|
row: ElementRef<'_>,
|
||||||
@@ -427,18 +417,7 @@ fn parse_description(
|
|||||||
let mut description = if selector.contains("selector")? || selector.contains("selectors")? {
|
let mut description = if selector.contains("selector")? || selector.contains("selectors")? {
|
||||||
safe_query(row, &selector)?
|
safe_query(row, &selector)?
|
||||||
} else if let Some(template) = get_optional_string(&selector, "text")? {
|
} else if let Some(template) = get_optional_string(&selector, "text")? {
|
||||||
let mut values = Vec::new();
|
let values = collect_template_field_values(row, fields, &template)?;
|
||||||
for key in [
|
|
||||||
"tags",
|
|
||||||
"subject",
|
|
||||||
"description_free_forever",
|
|
||||||
"description_normal",
|
|
||||||
] {
|
|
||||||
if let Some(field_selector) = get_field_dict(fields, key)? {
|
|
||||||
let value = safe_query(row, &field_selector)?.unwrap_or_default();
|
|
||||||
values.push((key.to_string(), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let refs: Vec<(&str, &str)> = values
|
let refs: Vec<(&str, &str)> = values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||||
@@ -457,6 +436,33 @@ fn parse_description(
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按 Jinja 模板实际引用的 fields 字段提取当前行数据,避免把模板能力绑死在固定字段名上。
|
||||||
|
fn collect_template_field_values(
|
||||||
|
row: ElementRef<'_>,
|
||||||
|
fields: &Bound<'_, PyDict>,
|
||||||
|
template: &str,
|
||||||
|
) -> PyResult<Vec<(String, String)>> {
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
for captures in FIELD_REF_RE.captures_iter(template) {
|
||||||
|
let Some(key) = captures.get(1).or_else(|| captures.get(2)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let key = key.as_str();
|
||||||
|
if !keys.iter().any(|item: &String| item == key) {
|
||||||
|
keys.push(key.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values = Vec::new();
|
||||||
|
for key in keys {
|
||||||
|
if let Some(field_selector) = get_field_dict(fields, &key)? {
|
||||||
|
let value = safe_query(row, &field_selector)?.unwrap_or_default();
|
||||||
|
values.push((key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
|
||||||
/// 解析普通文本字段。
|
/// 解析普通文本字段。
|
||||||
fn parse_plain_field(
|
fn parse_plain_field(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
@@ -1112,7 +1118,11 @@ fn eval_field_atom(expression: &str, values: &[(&str, &str)]) -> Option<String>
|
|||||||
return Some(value);
|
return Some(value);
|
||||||
}
|
}
|
||||||
let key = parse_field_key(expression)?;
|
let key = parse_field_key(expression)?;
|
||||||
Some(get_template_value(values, &key).unwrap_or_default().to_string())
|
Some(
|
||||||
|
get_template_value(values, &key)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析单引号或双引号字符串字面量。
|
/// 解析单引号或双引号字符串字面量。
|
||||||
|
|||||||
@@ -249,6 +249,56 @@ def test_rust_indexer_page_parser_renders_common_title_template():
|
|||||||
assert [item["title"] for item in torrents] == ["Optional Name", "Default Fallback"]
|
assert [item["title"] for item in torrents] == ["Optional Name", "Default Fallback"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_rust_indexer_page_parser_renders_literal_title_template_without_default_field():
|
||||||
|
"""
|
||||||
|
Rust 普通 indexer 页面解析应在没有 title_default 时渲染 title_optional 的纯文本兜底模板。
|
||||||
|
"""
|
||||||
|
spider = SiteSpider(
|
||||||
|
indexer={
|
||||||
|
"id": "demo",
|
||||||
|
"name": "Demo",
|
||||||
|
"domain": "https://example.org/",
|
||||||
|
"search": {"paths": [{"path": "torrents.php"}]},
|
||||||
|
"torrents": {
|
||||||
|
"list": {"selector": "tr.torrent"},
|
||||||
|
"fields": {
|
||||||
|
"title_optional": {
|
||||||
|
"selector": "a.title",
|
||||||
|
"attribute": "title",
|
||||||
|
"optional": True,
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"text": (
|
||||||
|
"{% if fields['title_optional'] %}"
|
||||||
|
"{{ fields['title_optional'] }}"
|
||||||
|
"{% else %}"
|
||||||
|
"For All Mankind S05 2019 2160p ATVP WEB-DL "
|
||||||
|
"DDP5.1 Atmos DV H 265-HHWEB [新]"
|
||||||
|
"{% endif %}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"download": {"selector": "a.dl", "attribute": "href"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
html = """
|
||||||
|
<table>
|
||||||
|
<tr class="torrent">
|
||||||
|
<td><a class="title" title="" href="/details/1">Ignored</a></td>
|
||||||
|
<td><a class="dl" href="/download/1">DL</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
torrents = spider.parse(html)
|
||||||
|
|
||||||
|
assert torrents == [{
|
||||||
|
"title": "For All Mankind S05 2019 2160p ATVP WEB-DL DDP5.1 Atmos DV H 265-HHWEB [新]",
|
||||||
|
"enclosure": "https://example.org/download/1",
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
def test_rust_indexer_page_parser_renders_common_description_templates():
|
def test_rust_indexer_page_parser_renders_common_description_templates():
|
||||||
"""
|
"""
|
||||||
Rust 普通 indexer 页面解析应兼容站点构建项目里的 description 字段模板。
|
Rust 普通 indexer 页面解析应兼容站点构建项目里的 description 字段模板。
|
||||||
|
|||||||
Reference in New Issue
Block a user