mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-04 06:59:42 +08:00
Rust vs Python benchmark results: - RSS: Rust 0.299 ms/loop vs Python 7.913 ms/loop, 26.47x faster - Filter: Rust 12.740 ms/loop vs Python 57.187 ms/loop, 4.49x faster - MetaInfo: Rust 64.680 ms/loop vs Python 316.158 ms/loop, 4.89x faster - Indexer agsvpt: Rust 145.76 ms vs Python 3686.50 ms, 25.29x faster - Indexer pttime: Rust 166.51 ms vs Python 4019.87 ms, 24.14x faster - Indexer chdbits: Rust 161.17 ms vs Python 3604.28 ms, 22.36x faster - Indexer iptorrents: Rust 77.82 ms vs Python 17615.52 ms, 226.36x faster Validation: - cargo fmt/check/test for rust/moviepilot_rust - pytest Rust-related coverage: tests/test_rust_accel.py tests/test_torrent_filter.py tests/test_metainfo.py tests/test_indexer_spider_search_url.py tests/test_workflow_fetch_rss.py - tests/run.py legacy suite - pylint app/ --errors-only
216 lines
6.2 KiB
Rust
216 lines
6.2 KiB
Rust
use pyo3::prelude::*;
|
||
use pyo3::types::{PyAny, PyDict, PyList};
|
||
use regex::Regex;
|
||
use std::collections::HashMap;
|
||
use std::sync::Mutex;
|
||
|
||
/// 从 Python 字典读取可选字符串。
|
||
pub(crate) fn get_optional_string(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<String>> {
|
||
let Some(value) = dict.get_item(key)? else {
|
||
return Ok(None);
|
||
};
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
Ok(Some(value.str()?.to_str()?.to_string()))
|
||
}
|
||
|
||
/// 从 Python 字典读取非空字符串。
|
||
pub(crate) fn get_optional_nonempty_string(
|
||
dict: &Bound<'_, PyDict>,
|
||
key: &str,
|
||
) -> PyResult<Option<String>> {
|
||
let Some(value) = get_optional_string(dict, key)? else {
|
||
return Ok(None);
|
||
};
|
||
let value = value.trim().to_string();
|
||
if value.is_empty() {
|
||
Ok(None)
|
||
} else {
|
||
Ok(Some(value))
|
||
}
|
||
}
|
||
|
||
/// 从 Python 字典读取可选整数。
|
||
pub(crate) fn get_optional_i64(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<i64>> {
|
||
let Some(value) = dict.get_item(key)? else {
|
||
return Ok(None);
|
||
};
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
if let Ok(parsed) = value.extract::<i64>() {
|
||
return Ok(Some(parsed));
|
||
}
|
||
let text = value.str()?.to_str()?.trim().to_string();
|
||
if text.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
Ok(text.parse::<i64>().ok())
|
||
}
|
||
|
||
/// 从 Python 字典读取可选浮点数。
|
||
pub(crate) fn get_optional_f64(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<f64>> {
|
||
let Some(value) = dict.get_item(key)? else {
|
||
return Ok(None);
|
||
};
|
||
py_any_to_f64(&value)
|
||
}
|
||
|
||
/// 从 Python 字典读取字符串列表,兼容单值字符串。
|
||
pub(crate) fn get_string_list(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<String>> {
|
||
let Some(value) = dict.get_item(key)? else {
|
||
return Ok(Vec::new());
|
||
};
|
||
py_any_to_string_list(&value)
|
||
}
|
||
|
||
/// 从 Python 字典读取配置字符串列表,兼容列表和以换行、竖线、分号分隔的字符串。
|
||
pub(crate) fn get_config_string_list(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<String>> {
|
||
let Some(value) = dict.get_item(key)? else {
|
||
return Ok(Vec::new());
|
||
};
|
||
if value.is_none() {
|
||
return Ok(Vec::new());
|
||
}
|
||
if let Ok(list) = value.downcast::<PyList>() {
|
||
let mut result = Vec::new();
|
||
for item in list.iter() {
|
||
let text = item.extract::<String>()?;
|
||
if !text.is_empty() {
|
||
result.push(text);
|
||
}
|
||
}
|
||
return Ok(result);
|
||
}
|
||
if let Ok(text) = value.extract::<String>() {
|
||
return Ok(text
|
||
.replace('\n', ";")
|
||
.replace('|', ";")
|
||
.split(';')
|
||
.filter_map(|item| {
|
||
let item = item.trim();
|
||
(!item.is_empty()).then(|| item.to_string())
|
||
})
|
||
.collect());
|
||
}
|
||
Ok(Vec::new())
|
||
}
|
||
|
||
/// 将 Python 对象转换为 i64,用于兼容配置里字符串或数字形式的下标。
|
||
pub(crate) fn extract_i64(value: &Bound<'_, PyAny>) -> PyResult<Option<i64>> {
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
if let Ok(parsed) = value.extract::<i64>() {
|
||
return Ok(Some(parsed));
|
||
}
|
||
let text = value.str()?.to_str()?.trim().to_string();
|
||
if text.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
Ok(text.parse::<i64>().ok())
|
||
}
|
||
|
||
/// 将 Python 值转换为可选 i64。
|
||
pub(crate) fn py_any_to_i64(value: &Bound<'_, PyAny>) -> PyResult<Option<i64>> {
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
if let Ok(parsed) = value.extract::<i64>() {
|
||
return Ok(Some(parsed));
|
||
}
|
||
let text = value.str()?.to_str()?.trim().to_string();
|
||
if text.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
Ok(text.parse::<i64>().ok())
|
||
}
|
||
|
||
/// 将 Python 值转换为可选 f64。
|
||
pub(crate) fn py_any_to_f64(value: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
if let Ok(parsed) = value.extract::<f64>() {
|
||
return Ok(Some(parsed));
|
||
}
|
||
let text = value.str()?.to_str()?.trim().to_string();
|
||
if text.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
Ok(text.parse::<f64>().ok())
|
||
}
|
||
|
||
/// 将 Python 值转换为字符串列表。
|
||
pub(crate) fn py_any_to_string_list(value: &Bound<'_, PyAny>) -> PyResult<Vec<String>> {
|
||
if value.is_none() {
|
||
return Ok(Vec::new());
|
||
}
|
||
if let Ok(list) = value.downcast::<PyList>() {
|
||
let mut result = Vec::new();
|
||
for item in list.iter() {
|
||
let text = item.str()?.to_str()?.to_string();
|
||
if !text.is_empty() {
|
||
result.push(text);
|
||
}
|
||
}
|
||
return Ok(result);
|
||
}
|
||
let text = value.str()?.to_str()?.to_string();
|
||
if text.is_empty() {
|
||
Ok(Vec::new())
|
||
} else {
|
||
Ok(vec![text])
|
||
}
|
||
}
|
||
|
||
/// 从对象读取可选字符串属性。
|
||
pub(crate) fn object_optional_string(
|
||
obj: &Bound<'_, PyAny>,
|
||
attr: &str,
|
||
) -> PyResult<Option<String>> {
|
||
let value = obj.getattr(attr)?;
|
||
if value.is_none() {
|
||
return Ok(None);
|
||
}
|
||
let text = value.str()?.to_str()?.to_string();
|
||
if text.is_empty() {
|
||
Ok(None)
|
||
} else {
|
||
Ok(Some(text))
|
||
}
|
||
}
|
||
|
||
/// 从对象读取可选字符串列表属性。
|
||
pub(crate) fn object_string_list(obj: &Bound<'_, PyAny>, attr: &str) -> PyResult<Vec<String>> {
|
||
let value = obj.getattr(attr)?;
|
||
py_any_to_string_list(&value)
|
||
}
|
||
|
||
/// 从对象读取可选整数属性。
|
||
pub(crate) fn object_optional_i64(obj: &Bound<'_, PyAny>, attr: &str) -> PyResult<Option<i64>> {
|
||
let value = obj.getattr(attr)?;
|
||
py_any_to_i64(&value)
|
||
}
|
||
|
||
/// 从对象读取可选浮点属性。
|
||
pub(crate) fn object_optional_f64(obj: &Bound<'_, PyAny>, attr: &str) -> PyResult<Option<f64>> {
|
||
let value = obj.getattr(attr)?;
|
||
py_any_to_f64(&value)
|
||
}
|
||
|
||
/// 按正则文本缓存动态正则,避免热路径重复编译。
|
||
pub(crate) fn cached_regex(cache: &Mutex<HashMap<String, Regex>>, pattern: &str) -> Option<Regex> {
|
||
if let Ok(guard) = cache.lock() {
|
||
if let Some(regex) = guard.get(pattern) {
|
||
return Some(regex.clone());
|
||
}
|
||
}
|
||
let regex = Regex::new(pattern).ok()?;
|
||
if let Ok(mut guard) = cache.lock() {
|
||
guard.insert(pattern.to_string(), regex.clone());
|
||
}
|
||
Some(regex)
|
||
}
|