mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-02 05:56:47 +08:00
refactor: split torrent cache refresh stages
This commit is contained in:
+120
-142
@@ -526,6 +526,118 @@ class TorrentsChain(ChainBase):
|
||||
torrents.append(torrent)
|
||||
return torrents
|
||||
|
||||
def _refresh_indexer(
|
||||
self,
|
||||
indexer: dict,
|
||||
stype: str,
|
||||
include_music: bool,
|
||||
torrents_cache: Dict[str, List[Context]],
|
||||
music_cache: Dict[str, List[Context]],
|
||||
) -> str:
|
||||
"""抓取并写入单个站点的影视、音乐资源缓存。"""
|
||||
domain = site_rules.extract_domain(indexer.get("domain"))
|
||||
if stype == "spider":
|
||||
torrents: List[TorrentInfo] = []
|
||||
for page in range(2):
|
||||
page_torrents = self.browse(domain=domain, page=page)
|
||||
if not page_torrents:
|
||||
break
|
||||
torrents.extend(page_torrents)
|
||||
else:
|
||||
torrents = self.rss(domain=domain)
|
||||
if include_music and self._music_browse_paths(indexer):
|
||||
torrents = self.__append_music_browse_torrents(domain=domain, torrents=torrents)
|
||||
torrents.sort(key=lambda item: item.pubdate or "", reverse=True)
|
||||
music_torrents = [
|
||||
item for item in torrents if item.category == MediaType.MUSIC.value
|
||||
][:self.runtime_config.refresh_batch_size]
|
||||
torrents = [
|
||||
item for item in torrents if item.category != MediaType.MUSIC.value
|
||||
][:self.runtime_config.refresh_batch_size]
|
||||
if not torrents and not music_torrents:
|
||||
logger.info(f'{indexer.get("name")} 没有获取到种子')
|
||||
return domain
|
||||
if self._is_no_cache_site(domain):
|
||||
logger.info(
|
||||
f'{indexer.get("name")} 有 {len(torrents) + len(music_torrents)} 个种子 (不缓存)'
|
||||
)
|
||||
torrents_cache[domain] = []
|
||||
music_cache[domain] = []
|
||||
else:
|
||||
cached_signatures = {
|
||||
f'{item.torrent_info.title}{item.torrent_info.description}'
|
||||
for item in torrents_cache.get(domain) or []
|
||||
}
|
||||
torrents = [
|
||||
item for item in torrents
|
||||
if f'{item.title}{item.description}' not in cached_signatures
|
||||
]
|
||||
music_signatures = {
|
||||
f'{item.torrent_info.title}{item.torrent_info.description}'
|
||||
for item in music_cache.get(domain) or []
|
||||
}
|
||||
music_torrents = [
|
||||
item for item in music_torrents
|
||||
if f'{item.title}{item.description}' not in music_signatures
|
||||
]
|
||||
if not torrents and not music_torrents:
|
||||
logger.info(f'{indexer.get("name")} 没有新种子')
|
||||
return domain
|
||||
logger.info(f'{indexer.get("name")} 有 {len(torrents) + len(music_torrents)} 个新种子')
|
||||
for torrent in torrents + music_torrents:
|
||||
if global_vars.is_system_stopped:
|
||||
break
|
||||
if not torrent.enclosure:
|
||||
logger.warning(f"缺少种子链接,忽略处理: {torrent.title}")
|
||||
continue
|
||||
context = self._build_refresh_context(torrent, stype)
|
||||
target_cache = music_cache if torrent.category == MediaType.MUSIC.value else torrents_cache
|
||||
target_cache.setdefault(domain, []).append(context)
|
||||
if len(target_cache[domain]) > self.runtime_config.torrent_cache_size:
|
||||
target_cache[domain] = target_cache[domain][-self.runtime_config.torrent_cache_size:]
|
||||
return domain
|
||||
|
||||
def _is_no_cache_site(self, domain: str) -> bool:
|
||||
"""判断站点是否配置为不缓存资源。"""
|
||||
return any(key in domain for key in self.runtime_config.no_cache_site_key.split(","))
|
||||
|
||||
def _build_refresh_context(self, torrent: TorrentInfo, stype: str) -> Context:
|
||||
"""识别单个种子并构造缓存上下文。"""
|
||||
logger.info(f'处理资源:{torrent.title} ...')
|
||||
if torrent.category == MediaType.MUSIC.value:
|
||||
meta = MetaMusic.parse_query(torrent.title)
|
||||
mediainfo = MusicInfo(
|
||||
title=meta.title,
|
||||
artists=list(meta.artists),
|
||||
album=meta.album,
|
||||
year=meta.year,
|
||||
names=[meta.title] if meta.title else [],
|
||||
)
|
||||
candidate_recognized = False
|
||||
match_source = "unknown"
|
||||
else:
|
||||
meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
|
||||
if torrent.title != meta.org_string:
|
||||
logger.info(f'种子名称应用识别词后发生改变:{torrent.title} => {meta.org_string}')
|
||||
if meta.type != MediaType.TV and torrent.category == MediaType.TV.value:
|
||||
meta.type = MediaType.TV
|
||||
mediainfo = MediaChain().recognize_by_meta(meta, obtain_images=False) or MediaInfo()
|
||||
mediainfo.clear()
|
||||
candidate_recognized = bool(mediainfo and all(resolve_media_identity(media=mediainfo)))
|
||||
match_source = self._get_media_id_match_source(mediainfo)
|
||||
context = Context(
|
||||
meta_info=meta,
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrent,
|
||||
resource_source="spider" if stype == "spider" else "rss",
|
||||
match_source=match_source if candidate_recognized else "unknown",
|
||||
candidate_recognized=candidate_recognized,
|
||||
media_info_is_target=False,
|
||||
)
|
||||
if not mediainfo or not all(resolve_media_identity(media=mediainfo)):
|
||||
context.media_recognize_fail_count = 1
|
||||
return context
|
||||
|
||||
def refresh(
|
||||
self,
|
||||
stype: Optional[str] = None,
|
||||
@@ -541,15 +653,6 @@ class TorrentsChain(ChainBase):
|
||||
:param include_music: 是否额外抓取站点的音乐专用浏览入口,服务音乐订阅
|
||||
"""
|
||||
|
||||
def __is_no_cache_site(_domain: str) -> bool:
|
||||
"""
|
||||
判断站点是否不需要缓存
|
||||
"""
|
||||
for url_key in self.runtime_config.no_cache_site_key.split(','):
|
||||
if url_key in _domain:
|
||||
return True
|
||||
return False
|
||||
|
||||
# 刷新类型
|
||||
if not stype:
|
||||
stype = self.runtime_config.subscribe_mode
|
||||
@@ -594,145 +697,20 @@ class TorrentsChain(ChainBase):
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
value=(index - 1) / total_indexers * 100 if total_indexers else 100,
|
||||
text=(
|
||||
f"正在刷新站点资源({index}/{total_indexers})"
|
||||
f"{indexer.get('name')} ..."
|
||||
),
|
||||
text=f"正在刷新站点资源({index}/{total_indexers}){indexer.get('name')} ...",
|
||||
data={
|
||||
"total": total_indexers,
|
||||
"finished": index - 1,
|
||||
"current": indexer.get("id"),
|
||||
},
|
||||
)
|
||||
domain = site_rules.extract_domain(indexer.get("domain"))
|
||||
domains.append(domain)
|
||||
if stype == "spider":
|
||||
# 刷新首页种子
|
||||
torrents: List[TorrentInfo] = []
|
||||
# 读取第0页和第1页
|
||||
for page in range(2):
|
||||
page_torrents = self.browse(domain=domain, page=page)
|
||||
if page_torrents:
|
||||
torrents.extend(page_torrents)
|
||||
else:
|
||||
# 如果某一页没有数据,说明已经到最后一页,停止获取
|
||||
break
|
||||
# 存在音乐订阅时,默认首页可能不包含音乐资源,需要额外抓取音乐专用入口
|
||||
if include_music and self._music_browse_paths(indexer):
|
||||
torrents = self.__append_music_browse_torrents(
|
||||
domain=domain, torrents=torrents
|
||||
)
|
||||
else:
|
||||
# 刷新RSS种子
|
||||
torrents: List[TorrentInfo] = self.rss(domain=domain)
|
||||
# 混合站点的 RSS 通常不提供媒体分类;有音乐订阅时补抓专用入口,
|
||||
# 后续仍按与 spider 相同的独立缓存和去重规则处理。
|
||||
if include_music and self._music_browse_paths(indexer):
|
||||
torrents = self.__append_music_browse_torrents(
|
||||
domain=domain, torrents=torrents
|
||||
)
|
||||
# 按pubdate降序排列
|
||||
torrents.sort(key=lambda x: x.pubdate or '', reverse=True)
|
||||
# 音乐与影视按同一公共参数独立计算刷新配额,并分别写入各自缓存,音乐不会被影视资源挤出
|
||||
music_torrents = [
|
||||
t for t in torrents if t.category == MediaType.MUSIC.value
|
||||
][:self.runtime_config.refresh_batch_size]
|
||||
torrents = [
|
||||
t for t in torrents if t.category != MediaType.MUSIC.value
|
||||
][:self.runtime_config.refresh_batch_size]
|
||||
if torrents or music_torrents:
|
||||
if __is_no_cache_site(domain):
|
||||
# 不需要缓存的站点,直接处理
|
||||
logger.info(f'{indexer.get("name")} 有 {len(torrents) + len(music_torrents)} 个种子 (不缓存)')
|
||||
torrents_cache[domain] = []
|
||||
music_cache[domain] = []
|
||||
else:
|
||||
# 过滤出没有处理过的种子 - 优化:使用集合查找,避免重复创建字符串列表
|
||||
cached_signatures = {f'{t.torrent_info.title}{t.torrent_info.description}'
|
||||
for t in torrents_cache.get(domain) or []}
|
||||
torrents = [torrent for torrent in torrents
|
||||
if f'{torrent.title}{torrent.description}' not in cached_signatures]
|
||||
# 音乐种子对照音乐独立缓存去重
|
||||
music_signatures = {f'{t.torrent_info.title}{t.torrent_info.description}'
|
||||
for t in music_cache.get(domain) or []}
|
||||
music_torrents = [torrent for torrent in music_torrents
|
||||
if f'{torrent.title}{torrent.description}' not in music_signatures]
|
||||
if torrents or music_torrents:
|
||||
logger.info(f'{indexer.get("name")} 有 {len(torrents) + len(music_torrents)} 个新种子')
|
||||
else:
|
||||
logger.info(f'{indexer.get("name")} 没有新种子')
|
||||
continue
|
||||
try:
|
||||
for torrent in torrents + music_torrents:
|
||||
if global_vars.is_system_stopped:
|
||||
break
|
||||
if not torrent.enclosure:
|
||||
logger.warn(f"缺少种子链接,忽略处理: {torrent.title}")
|
||||
continue
|
||||
logger.info(f'处理资源:{torrent.title} ...')
|
||||
if torrent.category == MediaType.MUSIC.value:
|
||||
meta = MetaMusic.parse_query(torrent.title)
|
||||
mediainfo = MusicInfo(
|
||||
title=meta.title,
|
||||
artists=list(meta.artists),
|
||||
album=meta.album,
|
||||
year=meta.year,
|
||||
names=[meta.title] if meta.title else [],
|
||||
)
|
||||
candidate_recognized = False
|
||||
match_source = "unknown"
|
||||
else:
|
||||
meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
|
||||
if torrent.title != meta.org_string:
|
||||
logger.info(f'种子名称应用识别词后发生改变:{torrent.title} => {meta.org_string}')
|
||||
# 使用站点种子分类,校正类型识别
|
||||
if meta.type != MediaType.TV \
|
||||
and torrent.category == MediaType.TV.value:
|
||||
meta.type = MediaType.TV
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
meta,
|
||||
obtain_images=False,
|
||||
)
|
||||
if not mediainfo:
|
||||
logger.warn(f'{torrent.title} 未识别到媒体信息')
|
||||
mediainfo = MediaInfo()
|
||||
mediainfo.clear()
|
||||
candidate_recognized = bool(
|
||||
mediainfo and all(resolve_media_identity(media=mediainfo))
|
||||
)
|
||||
match_source = self._get_media_id_match_source(mediainfo)
|
||||
# 上下文
|
||||
context = Context(
|
||||
meta_info=meta,
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrent,
|
||||
resource_source="spider" if stype == "spider" else "rss",
|
||||
match_source=match_source if candidate_recognized else "unknown",
|
||||
candidate_recognized=candidate_recognized,
|
||||
media_info_is_target=False,
|
||||
)
|
||||
# 如果未识别到媒体信息,设置初始失败次数为1
|
||||
if not mediainfo or not all(resolve_media_identity(media=mediainfo)):
|
||||
context.media_recognize_fail_count = 1
|
||||
# 添加到缓存:音乐进入独立缓存,与影视分开存储
|
||||
if torrent.category == MediaType.MUSIC.value:
|
||||
target_cache = music_cache
|
||||
else:
|
||||
target_cache = torrents_cache
|
||||
if not target_cache.get(domain):
|
||||
target_cache[domain] = [context]
|
||||
else:
|
||||
target_cache[domain].append(context)
|
||||
# 如果超过了限制条数则移除掉前面的,音乐与影视各自独立计算配额
|
||||
if len(target_cache[domain]) > self.runtime_config.torrent_cache_size:
|
||||
target_cache[domain] = target_cache[domain][-self.runtime_config.torrent_cache_size:]
|
||||
finally:
|
||||
torrents.clear()
|
||||
music_torrents.clear()
|
||||
del torrents
|
||||
del music_torrents
|
||||
else:
|
||||
logger.info(f'{indexer.get("name")} 没有获取到种子')
|
||||
domains.append(self._refresh_indexer(
|
||||
indexer=indexer,
|
||||
stype=stype,
|
||||
include_music=include_music,
|
||||
torrents_cache=torrents_cache,
|
||||
music_cache=music_cache,
|
||||
))
|
||||
|
||||
# 保存缓存到本地,影视与音乐分别存储
|
||||
if stype == "spider":
|
||||
|
||||
@@ -971,6 +971,8 @@ Outbox adapter、DB 装饰器、Base 与 UoW,strict 清单扩大到 37 个源
|
||||
对应 `mcp_jsonrpc`、`download.add`、`DownloadChain.get_no_exists_info` 退出超限清单,总债务从 28 降到 25。
|
||||
- 2026-08-22 将 `SiteChain.sync_cookies` 拆为单域名处理、黑名单判断、索引器地址解析和连接重试阶段,入口降至预算内;
|
||||
保留已有站点健康、黑名单、失败重试时的事件与进度回调语义,站点专项测试通过。
|
||||
- 继续将 `TorrentsChain.refresh` 拆为单站点抓取、上下文构造和缓存写入阶段,入口退出超限清单;
|
||||
音乐双缓存、去重、停止信号和订阅匹配专项测试通过,当前复杂度债务由 25 项降至 21 项。
|
||||
|
||||
配置债务继续按模块族收敛:`app/application/image.py` 的壁纸模式、图片缓存、代理和安全后缀读取已接入
|
||||
`ChainRuntimeConfig`,canonical `settings` 直接读取文件数从 137 降至 136;配置/依赖基线已更新,壁纸与图片专项测试通过。
|
||||
|
||||
+5
-9
@@ -2,8 +2,8 @@
|
||||
"api_endpoint": {
|
||||
"app/api/endpoints/agent.py:web_agent_stream": 346,
|
||||
"app/api/endpoints/media.py:scrape": 105,
|
||||
"app/api/endpoints/openai.py:chat_completions": 107,
|
||||
"app/api/endpoints/openai.py:responses": 105,
|
||||
"app/api/endpoints/openai.py:chat_completions": 95,
|
||||
"app/api/endpoints/openai.py:responses": 93,
|
||||
"app/api/endpoints/system.py:get_logging": 111,
|
||||
"app/api/endpoints/system.py:nettest": 87,
|
||||
"app/api/endpoints/torrent.py:reidentify_cache": 147
|
||||
@@ -20,14 +20,10 @@
|
||||
"app/chain/download.py:DownloadChain.batch_download": 572,
|
||||
"app/chain/download.py:DownloadChain.download_single": 255,
|
||||
"app/chain/mediaserver.py:MediaServerChain.sync": 292,
|
||||
"app/chain/site.py:SiteChain.sync_cookies": 180,
|
||||
"app/chain/subscribe.py:SubscribeChain.add": 183,
|
||||
"app/chain/subscribe.py:SubscribeChain.async_add": 186,
|
||||
"app/chain/subscribe.py:SubscribeChain.match": 417,
|
||||
"app/chain/subscribe.py:SubscribeChain.search": 249,
|
||||
"app/chain/subscribe.py:SubscribeChain.subscribe_files_info": 199,
|
||||
"app/chain/torrents.py:TorrentsChain.refresh": 235,
|
||||
"app/chain/transfer.py:TransferChain.do_transfer": 885,
|
||||
"app/chain/transfer.py:TransferChain.process": 154
|
||||
"app/chain/subscribe.py:SubscribeChain.match": 415,
|
||||
"app/chain/subscribe.py:SubscribeChain.search": 246,
|
||||
"app/chain/transfer.py:TransferChain.do_transfer": 885
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user