mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-12 16:02:35 +08:00
refactor: slim rust acceleration surface
This commit is contained in:
@@ -11,7 +11,6 @@ from app.schemas.types import MediaType
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.tokens import Tokens
|
||||
from app.core.meta.streamingplatform import StreamingPlatforms
|
||||
from app.utils import rust_accel
|
||||
|
||||
|
||||
class MetaVideo(MetaBase):
|
||||
@@ -104,60 +103,58 @@ class MetaVideo(MetaBase):
|
||||
# 把年月日去掉
|
||||
title = re.sub(r'\d{4}[\s._-]\d{1,2}[\s._-]\d{1,2}', "", title)
|
||||
media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIOEXT
|
||||
rust_parse = rust_accel.parse_video_title(title, isfile=isfile, media_exts=media_exts)
|
||||
if not self.__apply_rust_parse(rust_parse):
|
||||
# 拆分tokens
|
||||
tokens = Tokens(title)
|
||||
# 实例化StreamingPlatforms对象
|
||||
streaming_platforms = StreamingPlatforms()
|
||||
# 解析名称、年份、季、集、资源类型、分辨率等
|
||||
# 拆分tokens
|
||||
tokens = Tokens(title)
|
||||
# 实例化StreamingPlatforms对象
|
||||
streaming_platforms = StreamingPlatforms()
|
||||
# 解析名称、年份、季、集、资源类型、分辨率等
|
||||
token = tokens.get_next()
|
||||
while token:
|
||||
self._index += 1 # 更新当前处理的token索引
|
||||
# Part
|
||||
self.__init_part(token, tokens)
|
||||
# 标题
|
||||
if self._continue_flag:
|
||||
self.__init_name(token, media_exts)
|
||||
# 年份
|
||||
if self._continue_flag:
|
||||
self.__init_year(token)
|
||||
# 分辨率
|
||||
if self._continue_flag:
|
||||
self.__init_resource_pix(token)
|
||||
# 季
|
||||
if self._continue_flag:
|
||||
self.__init_season(token)
|
||||
# 集
|
||||
if self._continue_flag:
|
||||
self.__init_episode(token)
|
||||
# 资源类型
|
||||
if self._continue_flag:
|
||||
self.__init_resource_type(token)
|
||||
# 流媒体平台
|
||||
if self._continue_flag:
|
||||
self.__init_web_source(token, tokens, streaming_platforms)
|
||||
# 视频编码
|
||||
if self._continue_flag:
|
||||
self.__init_video_encode(token)
|
||||
# 视频位深
|
||||
if self._continue_flag:
|
||||
self.__init_video_bit(token)
|
||||
# 音频编码
|
||||
if self._continue_flag:
|
||||
self.__init_audio_encode(token)
|
||||
# 帧率
|
||||
if self._continue_flag:
|
||||
self.__init_fps(token)
|
||||
# 取下一个,直到没有为卡
|
||||
token = tokens.get_next()
|
||||
while token:
|
||||
self._index += 1 # 更新当前处理的token索引
|
||||
# Part
|
||||
self.__init_part(token, tokens)
|
||||
# 标题
|
||||
if self._continue_flag:
|
||||
self.__init_name(token, media_exts)
|
||||
# 年份
|
||||
if self._continue_flag:
|
||||
self.__init_year(token)
|
||||
# 分辨率
|
||||
if self._continue_flag:
|
||||
self.__init_resource_pix(token)
|
||||
# 季
|
||||
if self._continue_flag:
|
||||
self.__init_season(token)
|
||||
# 集
|
||||
if self._continue_flag:
|
||||
self.__init_episode(token)
|
||||
# 资源类型
|
||||
if self._continue_flag:
|
||||
self.__init_resource_type(token)
|
||||
# 流媒体平台
|
||||
if self._continue_flag:
|
||||
self.__init_web_source(token, tokens, streaming_platforms)
|
||||
# 视频编码
|
||||
if self._continue_flag:
|
||||
self.__init_video_encode(token)
|
||||
# 视频位深
|
||||
if self._continue_flag:
|
||||
self.__init_video_bit(token)
|
||||
# 音频编码
|
||||
if self._continue_flag:
|
||||
self.__init_audio_encode(token)
|
||||
# 帧率
|
||||
if self._continue_flag:
|
||||
self.__init_fps(token)
|
||||
# 取下一个,直到没有为卡
|
||||
token = tokens.get_next()
|
||||
self._continue_flag = True
|
||||
# 合成质量
|
||||
if self._effect:
|
||||
self._effect.reverse()
|
||||
self.resource_effect = " ".join(self._effect)
|
||||
if self._source:
|
||||
self.resource_type = self._source.strip()
|
||||
self._continue_flag = True
|
||||
# 合成质量
|
||||
if self._effect:
|
||||
self._effect.reverse()
|
||||
self.resource_effect = " ".join(self._effect)
|
||||
if self._source:
|
||||
self.resource_type = self._source.strip()
|
||||
# 提取原盘DIY
|
||||
if self.resource_type and "BluRay" in self.resource_type:
|
||||
if (self.subtitle and re.findall(r'D[Ii]Y', self.subtitle)) \
|
||||
@@ -188,62 +185,6 @@ class MetaVideo(MetaBase):
|
||||
if not self.video_bit:
|
||||
self.video_bit = self.extract_video_bit(self.video_encode)
|
||||
|
||||
def __apply_rust_parse(self, rust_parse: Optional[dict]) -> bool:
|
||||
"""
|
||||
应用 Rust 主识别结果;成功时跳过 Python token 主循环。
|
||||
"""
|
||||
if not rust_parse or not rust_parse.get("complete"):
|
||||
return False
|
||||
self.cn_name = rust_parse.get("cn_name")
|
||||
self.en_name = rust_parse.get("en_name")
|
||||
if rust_parse.get("year"):
|
||||
self.year = str(rust_parse.get("year"))
|
||||
self.part = rust_parse.get("part")
|
||||
self.__merge_rust_parse(rust_parse)
|
||||
media_type = rust_parse.get("type")
|
||||
if media_type == "tv":
|
||||
self.type = MediaType.TV
|
||||
elif media_type == "movie":
|
||||
self.type = MediaType.MOVIE
|
||||
return True
|
||||
|
||||
def __merge_rust_parse(self, rust_parse: Optional[dict]) -> None:
|
||||
"""
|
||||
合并 Rust 预解析结果,仅补齐 Python 识别未命中的资源字段。
|
||||
"""
|
||||
if not rust_parse:
|
||||
return
|
||||
if not self.year and rust_parse.get("year"):
|
||||
self.year = str(rust_parse.get("year"))
|
||||
if self.begin_season is None and rust_parse.get("begin_season") is not None:
|
||||
self.begin_season = int(rust_parse.get("begin_season"))
|
||||
self.type = MediaType.TV
|
||||
if self.end_season is None and rust_parse.get("end_season") is not None:
|
||||
self.end_season = int(rust_parse.get("end_season"))
|
||||
if not self.total_season and rust_parse.get("total_season"):
|
||||
self.total_season = int(rust_parse.get("total_season"))
|
||||
if self.begin_episode is None and rust_parse.get("begin_episode") is not None:
|
||||
self.begin_episode = int(rust_parse.get("begin_episode"))
|
||||
self.type = MediaType.TV
|
||||
if self.end_episode is None and rust_parse.get("end_episode") is not None:
|
||||
self.end_episode = int(rust_parse.get("end_episode"))
|
||||
if not self.total_episode and rust_parse.get("total_episode"):
|
||||
self.total_episode = int(rust_parse.get("total_episode"))
|
||||
if not self.resource_pix and rust_parse.get("resource_pix"):
|
||||
self.resource_pix = rust_parse.get("resource_pix")
|
||||
if not self.resource_type and rust_parse.get("resource_type"):
|
||||
self.resource_type = rust_parse.get("resource_type")
|
||||
if not self.resource_effect and rust_parse.get("resource_effect"):
|
||||
self.resource_effect = rust_parse.get("resource_effect")
|
||||
if not self.video_encode and rust_parse.get("video_encode"):
|
||||
self.video_encode = rust_parse.get("video_encode")
|
||||
if not self.video_bit and rust_parse.get("video_bit"):
|
||||
self.video_bit = rust_parse.get("video_bit")
|
||||
if not self.audio_encode and rust_parse.get("audio_encode"):
|
||||
self.audio_encode = rust_parse.get("audio_encode")
|
||||
if self.fps is None and rust_parse.get("fps") is not None:
|
||||
self.fps = int(rust_parse.get("fps"))
|
||||
|
||||
@staticmethod
|
||||
def __get_title_from_description(description: str) -> Optional[str]:
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,6 @@ from app.core.meta.infopath import (
|
||||
from app.core.meta.words import WordsMatcher
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils import rust_accel
|
||||
|
||||
|
||||
_ANIME_BRACKET_RE = re.compile(r'【[+0-9XVPI-]+】\s*【', re.IGNORECASE)
|
||||
@@ -169,9 +168,6 @@ def is_anime(name: str) -> bool:
|
||||
:param name: 名称
|
||||
:return: 是否动漫
|
||||
"""
|
||||
rust_result = rust_accel.is_anime(name)
|
||||
if rust_result is not None:
|
||||
return rust_result
|
||||
if not name:
|
||||
return False
|
||||
if _ANIME_BRACKET_RE.search(name):
|
||||
@@ -189,9 +185,6 @@ def find_metainfo(title: str) -> Tuple[str, dict]:
|
||||
"""
|
||||
从标题中提取媒体信息
|
||||
"""
|
||||
rust_result = rust_accel.find_metainfo(title)
|
||||
if rust_result is not None:
|
||||
return rust_result
|
||||
metainfo = _empty_metainfo()
|
||||
if not title:
|
||||
return title, metainfo
|
||||
|
||||
@@ -9,7 +9,6 @@ from lxml import etree
|
||||
from app.core.config import settings
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.log import logger
|
||||
from app.utils import rust_accel
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
@@ -228,32 +227,6 @@ class RssHelper:
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def __format_rust_items(items: List[dict]) -> List[dict]:
|
||||
"""
|
||||
将 Rust RSS 解析结果转换为原 Python XPath 解析返回结构。
|
||||
"""
|
||||
ret_array = []
|
||||
for item in items:
|
||||
pubdate = ""
|
||||
pubdate_raw = item.get("pubdate_raw")
|
||||
if pubdate_raw:
|
||||
pubdate = StringUtils.get_time(pubdate_raw)
|
||||
if pubdate is not None:
|
||||
pubdate = pubdate.astimezone(tz=None)
|
||||
tmp_dict = {
|
||||
'title': item.get("title") or "",
|
||||
'enclosure': item.get("enclosure") or "",
|
||||
'size': item.get("size") or 0,
|
||||
'description': item.get("description") or "",
|
||||
'link': item.get("link") or "",
|
||||
'pubdate': pubdate
|
||||
}
|
||||
if item.get("nickname"):
|
||||
tmp_dict['nickname'] = item.get("nickname")
|
||||
ret_array.append(tmp_dict)
|
||||
return ret_array
|
||||
|
||||
def parse(self, url, proxy: bool = False,
|
||||
timeout: Optional[int] = 15, headers: dict = None, ua: str = None) -> Union[List[dict], None, bool]:
|
||||
"""
|
||||
@@ -325,12 +298,6 @@ class RssHelper:
|
||||
logger.error("RSS内容不是有效的XML格式")
|
||||
return False
|
||||
|
||||
rust_items = rust_accel.parse_rss_items(ret_xml, self.MAX_RSS_ITEMS)
|
||||
if rust_items is not None:
|
||||
if len(rust_items) >= self.MAX_RSS_ITEMS:
|
||||
logger.warning(f"RSS条目过多,仅处理前{self.MAX_RSS_ITEMS}个")
|
||||
return self.__format_rust_items(rust_items)
|
||||
|
||||
# 使用lxml.etree解析XML
|
||||
parser = None
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,6 @@ from app.modules import _ModuleBase
|
||||
from app.modules.filter.RuleParser import RuleParser
|
||||
from app.modules.filter.builtin_rules import BUILTIN_RULE_SET
|
||||
from app.schemas.types import ModuleType, OtherModulesType, SystemConfigKey
|
||||
from app.utils import rust_accel
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
@@ -139,9 +138,6 @@ class FilterModule(_ModuleBase):
|
||||
# 查询规则表详情
|
||||
groups = self.rulehelper.get_rule_group_by_media(media=mediainfo, group_names=rule_groups)
|
||||
if groups:
|
||||
rust_filtered = self.__filter_torrents_by_rust(groups, torrent_list, mediainfo)
|
||||
if rust_filtered is not None:
|
||||
return rust_filtered
|
||||
for group in groups:
|
||||
# 过滤种子
|
||||
torrent_list = self.__filter_torrents(
|
||||
@@ -154,46 +150,6 @@ class FilterModule(_ModuleBase):
|
||||
)
|
||||
return torrent_list
|
||||
|
||||
def __filter_torrents_by_rust(self, groups: list, torrent_list: List[TorrentInfo],
|
||||
mediainfo: MediaInfo) -> Optional[List[TorrentInfo]]:
|
||||
"""
|
||||
使用 Rust 批量过滤种子;遇到不可支持的规则时返回 None 交由 Python 逻辑处理。
|
||||
"""
|
||||
if not torrent_list:
|
||||
return []
|
||||
payloads = [self.__build_rust_torrent_payload(torrent) for torrent in torrent_list]
|
||||
media_payload = mediainfo.to_dict() if mediainfo and hasattr(mediainfo, "to_dict") else (
|
||||
vars(mediainfo).copy() if mediainfo else None
|
||||
)
|
||||
result = rust_accel.filter_torrents(
|
||||
rule_set=self.rule_set,
|
||||
rule_strings=[group.rule_string for group in groups],
|
||||
torrents=payloads,
|
||||
media_info=media_payload,
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
filtered_torrents = []
|
||||
for index, pri_order in result:
|
||||
torrent = torrent_list[int(index)]
|
||||
torrent.pri_order = int(pri_order)
|
||||
filtered_torrents.append(torrent)
|
||||
return filtered_torrents
|
||||
|
||||
@staticmethod
|
||||
def __build_rust_torrent_payload(torrent: TorrentInfo) -> dict:
|
||||
"""
|
||||
组装 Rust 过滤器需要的纯数据载荷,避免 Rust 直接依赖 Python 业务对象。
|
||||
"""
|
||||
payload = torrent.to_dict() if hasattr(torrent, "to_dict") else vars(torrent).copy()
|
||||
payload["pub_minutes"] = torrent.pub_minutes()
|
||||
if payload.get("size"):
|
||||
meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
|
||||
payload["episode_count"] = meta.total_episode or 1
|
||||
else:
|
||||
payload["episode_count"] = 1
|
||||
return payload
|
||||
|
||||
def __filter_torrents(self, rule_string: str, rule_name: str,
|
||||
torrent_list: List[TorrentInfo],
|
||||
mediainfo: MediaInfo,
|
||||
|
||||
@@ -12,7 +12,6 @@ from pyquery import PyQuery
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils import rust_accel
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.url import UrlUtils
|
||||
@@ -96,19 +95,6 @@ class SiteSpider:
|
||||
"""
|
||||
获取搜索URL
|
||||
"""
|
||||
rust_url = rust_accel.build_indexer_search_url({
|
||||
"search": self.search,
|
||||
"batch": self.batch,
|
||||
"browse": self.browse,
|
||||
"category": self.category,
|
||||
"domain": self.domain,
|
||||
"keyword": self.keyword,
|
||||
"mtype": self.mtype.value if self.mtype else None,
|
||||
"cat": self.cat,
|
||||
"page": self.page,
|
||||
})
|
||||
if rust_url:
|
||||
return rust_url
|
||||
# 种子搜索相对路径
|
||||
paths = self.search.get('paths', [])
|
||||
torrentspath = ""
|
||||
@@ -753,16 +739,6 @@ class SiteSpider:
|
||||
|
||||
# 清空旧结果
|
||||
self.torrents_info_array = []
|
||||
rust_torrents = rust_accel.parse_indexer_torrents(
|
||||
html_text=html_text,
|
||||
domain=self.domain,
|
||||
list_config=self.list,
|
||||
fields=self.fields,
|
||||
category=self.category,
|
||||
result_num=int(self.result_num),
|
||||
)
|
||||
if rust_torrents is not None:
|
||||
return rust_torrents
|
||||
html_doc = None
|
||||
try:
|
||||
# 解析站点文本对象
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
try:
|
||||
import moviepilot_rust as _moviepilot_rust
|
||||
@@ -26,64 +25,6 @@ def import_error() -> Optional[Exception]:
|
||||
return _import_error
|
||||
|
||||
|
||||
def is_anime(name: str) -> Optional[bool]:
|
||||
"""
|
||||
使用 Rust 快路径判断标题是否为动漫格式,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return bool(_moviepilot_rust.is_anime_fast(name or ""))
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 动漫识别失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def find_metainfo(title: str) -> Optional[Tuple[str, Dict[str, Any]]]:
|
||||
"""
|
||||
使用 Rust 快路径提取标题中的内嵌媒体标签,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
result = _moviepilot_rust.find_metainfo_fast(title or "")
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 内嵌媒体标签识别失败,回退 Python:{err}")
|
||||
return None
|
||||
metainfo = {
|
||||
"tmdbid": result.get("tmdbid"),
|
||||
"doubanid": result.get("doubanid"),
|
||||
"type": _coerce_media_type(result.get("type")),
|
||||
"begin_season": result.get("begin_season"),
|
||||
"end_season": result.get("end_season"),
|
||||
"total_season": result.get("total_season"),
|
||||
"begin_episode": result.get("begin_episode"),
|
||||
"end_episode": result.get("end_episode"),
|
||||
"total_episode": result.get("total_episode"),
|
||||
}
|
||||
return result.get("title"), metainfo
|
||||
|
||||
|
||||
def parse_video_title(
|
||||
title: str,
|
||||
isfile: bool = False,
|
||||
media_exts: Optional[List[str]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
使用 Rust 执行影视标题主识别流程,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_video_title_fast(title or "", isfile, media_exts or [])
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 影视标题主识别失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_filter_rule(expression: str) -> Optional[list]:
|
||||
"""
|
||||
使用 Rust 解析过滤规则表达式,不可用时返回 None。
|
||||
@@ -98,92 +39,6 @@ def parse_filter_rule(expression: str) -> Optional[list]:
|
||||
return None
|
||||
|
||||
|
||||
def filter_torrents(
|
||||
rule_set: Dict[str, dict],
|
||||
rule_strings: List[str],
|
||||
torrents: List[dict],
|
||||
media_info: Optional[dict] = None,
|
||||
) -> Optional[list]:
|
||||
"""
|
||||
使用 Rust 批量执行种子过滤,不可用或不兼容时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.filter_torrents_fast(rule_set, rule_strings, torrents, media_info)
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 种子过滤失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def build_indexer_search_url(config: dict) -> Optional[str]:
|
||||
"""
|
||||
使用 Rust 根据普通 indexer 配置生成搜索 URL,不可用时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.build_indexer_search_url_fast(config)
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 站点搜索 URL 生成失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_indexer_torrents(
|
||||
html_text: str,
|
||||
domain: str,
|
||||
list_config: dict,
|
||||
fields: dict,
|
||||
category: Optional[dict],
|
||||
result_num: int,
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
使用 Rust 批量解析普通 indexer 页面,不支持的配置返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_indexer_torrents_fast(
|
||||
html_text or "",
|
||||
domain or "",
|
||||
list_config or {},
|
||||
fields or {},
|
||||
category,
|
||||
int(result_num or 0),
|
||||
)
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust 站点页面解析失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss_items(xml_text: str, max_items: int) -> Optional[List[dict]]:
|
||||
"""
|
||||
使用 Rust 批量解析 RSS/Atom 条目,不可用或解析失败时返回 None。
|
||||
"""
|
||||
if not _moviepilot_rust:
|
||||
return None
|
||||
try:
|
||||
return _moviepilot_rust.parse_rss_items_fast(xml_text or "", int(max_items or 0))
|
||||
except BaseException as err:
|
||||
_raise_non_rust_panic(err)
|
||||
logger.debug(f"Rust RSS 条目解析失败,回退 Python:{err}")
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_media_type(value: Optional[str]) -> Optional[MediaType]:
|
||||
"""
|
||||
将 Rust 返回的媒体类型字符串转换为系统 MediaType。
|
||||
"""
|
||||
if value == "movies":
|
||||
return MediaType.MOVIE
|
||||
if value == "tv":
|
||||
return MediaType.TV
|
||||
return None
|
||||
|
||||
|
||||
def _raise_non_rust_panic(err: BaseException) -> None:
|
||||
"""
|
||||
只吞掉 Rust 扩展 panic/异常,保留用户中断和进程退出语义。
|
||||
|
||||
Reference in New Issue
Block a user