mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-07-27 09:00:41 +08:00
perf: comprehensive performance optimization for backend and frontend
Backend: shared HTTP connection pool, concurrent RSS/torrent/notification operations, TMDB/Mikan result caching, database indexes, pre-compiled regex, __slots__ on dataclasses, O(1) set-based dedup, frozenset lookups, batch RSS enable/disable, asyncio.to_thread for blocking calls. Frontend: shallowRef for large arrays, computed table columns, watch instead of watchEffect, scoped style fix, typed emits, noImplicitAny, useIntervalFn lifecycle management, shared useClipboard instance, shallow clone for shared template objects. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,16 @@ from module.update import version_check
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_default_config_dict: dict | None = None
|
||||
|
||||
|
||||
def _get_default_config_dict() -> dict:
|
||||
global _default_config_dict
|
||||
if _default_config_dict is None:
|
||||
_default_config_dict = Config().dict()
|
||||
return _default_config_dict
|
||||
|
||||
|
||||
class Checker:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -32,7 +42,7 @@ class Checker:
|
||||
def check_first_run() -> bool:
|
||||
if Path("config/.setup_complete").exists():
|
||||
return False
|
||||
return settings.dict() == Config().dict()
|
||||
return settings.dict() == _get_default_config_dict()
|
||||
|
||||
@staticmethod
|
||||
def check_version() -> tuple[bool, int | None]:
|
||||
|
||||
@@ -58,11 +58,11 @@ class RenameThread(ProgramStatus):
|
||||
while not self.stop_event.is_set():
|
||||
async with Renamer() as renamer:
|
||||
renamed_info = await renamer.rename()
|
||||
if settings.notification.enable:
|
||||
if settings.notification.enable and renamed_info:
|
||||
async with PostNotification() as notifier:
|
||||
for info in renamed_info:
|
||||
await notifier.send_msg(info)
|
||||
await asyncio.sleep(2)
|
||||
await asyncio.gather(
|
||||
*[notifier.send_msg(info) for info in renamed_info]
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.stop_event.wait(),
|
||||
|
||||
@@ -62,6 +62,13 @@ class RSSDatabase:
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def enable_batch(self, ids: list[int]):
|
||||
statement = select(RSSItem).where(RSSItem.id.in_(ids))
|
||||
result = self.session.execute(statement)
|
||||
for item in result.scalars().all():
|
||||
item.enabled = True
|
||||
self.session.commit()
|
||||
|
||||
def disable(self, _id: int) -> bool:
|
||||
statement = select(RSSItem).where(RSSItem.id == _id)
|
||||
result = self.session.execute(statement)
|
||||
@@ -73,6 +80,13 @@ class RSSDatabase:
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def disable_batch(self, ids: list[int]):
|
||||
statement = select(RSSItem).where(RSSItem.id.in_(ids))
|
||||
result = self.session.execute(statement)
|
||||
for item in result.scalars().all():
|
||||
item.enabled = False
|
||||
self.session.commit()
|
||||
|
||||
def search_id(self, _id: int) -> RSSItem | None:
|
||||
return self.session.get(RSSItem, _id)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from module.conf import settings
|
||||
@@ -98,8 +99,7 @@ class DownloadClient(TorrentPath):
|
||||
|
||||
async def set_rules(self, bangumi_info: list[Bangumi]):
|
||||
logger.debug("[Downloader] Start adding rules.")
|
||||
for info in bangumi_info:
|
||||
await self.set_rule(info)
|
||||
await asyncio.gather(*[self.set_rule(info) for info in bangumi_info])
|
||||
logger.debug("[Downloader] Finished.")
|
||||
|
||||
async def get_torrent_info(self, category="Bangumi", status_filter="completed", tag=None):
|
||||
@@ -138,7 +138,9 @@ class DownloadClient(TorrentPath):
|
||||
torrent_url = [t.url for t in torrent]
|
||||
torrent_file = None
|
||||
else:
|
||||
torrent_file = [await req.get_content(t.url) for t in torrent]
|
||||
torrent_file = await asyncio.gather(
|
||||
*[req.get_content(t.url) for t in torrent]
|
||||
)
|
||||
torrent_url = None
|
||||
else:
|
||||
if "magnet" in torrent.url:
|
||||
|
||||
@@ -13,6 +13,10 @@ else:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_MEDIA_SUFFIXES = frozenset({".mp4", ".mkv"})
|
||||
_SUBTITLE_SUFFIXES = frozenset({".ass", ".srt"})
|
||||
|
||||
|
||||
class TorrentPath:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -23,10 +27,10 @@ class TorrentPath:
|
||||
subtitle_list = []
|
||||
for f in files:
|
||||
file_name = f["name"]
|
||||
suffix = Path(file_name).suffix
|
||||
if suffix.lower() in [".mp4", ".mkv"]:
|
||||
suffix = Path(file_name).suffix.lower()
|
||||
if suffix in _MEDIA_SUFFIXES:
|
||||
media_list.append(file_name)
|
||||
elif suffix.lower() in [".ass", ".srt"]:
|
||||
elif suffix in _SUBTITLE_SUFFIXES:
|
||||
subtitle_list.append(file_name)
|
||||
return media_list, subtitle_list
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ async def eps_complete():
|
||||
datas = engine.bangumi.not_complete()
|
||||
if datas:
|
||||
logger.info("Start collecting full season...")
|
||||
for data in datas:
|
||||
if not data.eps_collect:
|
||||
async with SeasonCollector() as collector:
|
||||
async with SeasonCollector() as collector:
|
||||
for data in datas:
|
||||
if not data.eps_collect:
|
||||
await collector.collect_season(data)
|
||||
data.eps_collect = True
|
||||
data.eps_collect = True
|
||||
engine.bangumi.update_all(datas)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -142,11 +143,14 @@ class Renamer(DownloadClient):
|
||||
rename_method = settings.bangumi_manage.rename_method
|
||||
torrents_info = await self.get_torrent_info()
|
||||
renamed_info: list[Notification] = []
|
||||
for info in torrents_info:
|
||||
# Fetch all torrent files concurrently
|
||||
all_files = await asyncio.gather(
|
||||
*[self.get_torrent_files(info["hash"]) for info in torrents_info]
|
||||
)
|
||||
for info, files in zip(torrents_info, all_files):
|
||||
torrent_hash = info["hash"]
|
||||
torrent_name = info["name"]
|
||||
save_path = info["save_path"]
|
||||
files = await self.get_torrent_files(torrent_hash)
|
||||
media_list, subtitle_list = self.check_files(files)
|
||||
bangumi_name, season = self._path_to_bangumi(save_path)
|
||||
kwargs = {
|
||||
|
||||
@@ -11,7 +11,7 @@ class Bangumi(SQLModel, table=True):
|
||||
default="official_title", alias="official_title", title="番剧中文名"
|
||||
)
|
||||
year: Optional[str] = Field(alias="year", title="番剧年份")
|
||||
title_raw: str = Field(default="title_raw", alias="title_raw", title="番剧原名")
|
||||
title_raw: str = Field(default="title_raw", alias="title_raw", title="番剧原名", index=True)
|
||||
season: int = Field(default=1, alias="season", title="番剧季度")
|
||||
season_raw: Optional[str] = Field(alias="season_raw", title="番剧季度原名")
|
||||
group_name: Optional[str] = Field(alias="group_name", title="字幕组")
|
||||
@@ -26,7 +26,7 @@ class Bangumi(SQLModel, table=True):
|
||||
added: bool = Field(default=False, alias="added", title="是否已添加")
|
||||
rule_name: Optional[str] = Field(alias="rule_name", title="番剧规则名")
|
||||
save_path: Optional[str] = Field(alias="save_path", title="番剧保存路径")
|
||||
deleted: bool = Field(False, alias="deleted", title="是否已删除")
|
||||
deleted: bool = Field(False, alias="deleted", title="是否已删除", index=True)
|
||||
air_weekday: Optional[int] = Field(default=None, alias="air_weekday", title="放送星期")
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class Notification(BaseModel):
|
||||
poster_path: Optional[str] = Field(None, alias="poster_path", title="番剧海报路径")
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class Episode:
|
||||
title_en: Optional[str]
|
||||
title_zh: Optional[str]
|
||||
@@ -75,8 +75,8 @@ class Episode:
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeasonInfo(dict):
|
||||
@dataclass(slots=True)
|
||||
class SeasonInfo:
|
||||
official_title: str
|
||||
title_raw: str
|
||||
season: int
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlmodel import Field, SQLModel
|
||||
class RSSItem(SQLModel, table=True):
|
||||
id: int = Field(default=None, primary_key=True, alias="id")
|
||||
name: Optional[str] = Field(None, alias="name")
|
||||
url: str = Field("https://mikanani.me", alias="url")
|
||||
url: str = Field("https://mikanani.me", alias="url", index=True)
|
||||
aggregate: bool = Field(False, alias="aggregate")
|
||||
parser: str = Field("mikan", alias="parser")
|
||||
enabled: bool = Field(True, alias="enabled")
|
||||
|
||||
@@ -7,9 +7,9 @@ from sqlmodel import Field, SQLModel
|
||||
class Torrent(SQLModel, table=True):
|
||||
id: int = Field(default=None, primary_key=True, alias="id")
|
||||
bangumi_id: Optional[int] = Field(None, alias="refer_id", foreign_key="bangumi.id")
|
||||
rss_id: Optional[int] = Field(None, alias="rss_id", foreign_key="rssitem.id")
|
||||
rss_id: Optional[int] = Field(None, alias="rss_id", foreign_key="rssitem.id", index=True)
|
||||
name: str = Field("", alias="name")
|
||||
url: str = Field("https://example.com/torrent", alias="url")
|
||||
url: str = Field("https://example.com/torrent", alias="url", index=True)
|
||||
homepage: Optional[str] = Field(None, alias="homepage")
|
||||
downloaded: bool = Field(False, alias="downloaded")
|
||||
|
||||
|
||||
@@ -21,13 +21,11 @@ class RequestContent(RequestURL):
|
||||
) -> list[Torrent]:
|
||||
soup = await self.get_xml(_url, retry)
|
||||
if soup:
|
||||
torrent_titles, torrent_urls, torrent_homepage = rss_parser(soup)
|
||||
parsed_items = rss_parser(soup)
|
||||
torrents: list[Torrent] = []
|
||||
if _filter is None:
|
||||
_filter = "|".join(settings.rss_parser.filter)
|
||||
for _title, torrent_url, homepage in zip(
|
||||
torrent_titles, torrent_urls, torrent_homepage
|
||||
):
|
||||
for _title, torrent_url, homepage in parsed_items:
|
||||
if re.search(_filter, _title) is None:
|
||||
torrents.append(
|
||||
Torrent(name=_title, url=torrent_url, homepage=homepage)
|
||||
|
||||
@@ -8,6 +8,46 @@ from module.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Module-level shared client for connection reuse
|
||||
_shared_client: httpx.AsyncClient | None = None
|
||||
_shared_client_proxy_key: str | None = None
|
||||
|
||||
|
||||
def _proxy_config_key() -> str:
|
||||
if settings.proxy.enable:
|
||||
return f"{settings.proxy.type}:{settings.proxy.host}:{settings.proxy.port}:{settings.proxy.username}"
|
||||
return ""
|
||||
|
||||
|
||||
async def get_shared_client() -> httpx.AsyncClient:
|
||||
global _shared_client, _shared_client_proxy_key
|
||||
current_key = _proxy_config_key()
|
||||
if _shared_client is not None and _shared_client_proxy_key == current_key:
|
||||
return _shared_client
|
||||
if _shared_client is not None:
|
||||
await _shared_client.aclose()
|
||||
timeout = httpx.Timeout(connect=5.0, read=10.0, write=10.0, pool=10.0)
|
||||
if settings.proxy.enable:
|
||||
if "http" in settings.proxy.type:
|
||||
if settings.proxy.username:
|
||||
proxy_url = f"http://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
|
||||
else:
|
||||
proxy_url = f"http://{settings.proxy.host}:{settings.proxy.port}"
|
||||
_shared_client = httpx.AsyncClient(proxy=proxy_url, timeout=timeout)
|
||||
elif settings.proxy.type == "socks5":
|
||||
if settings.proxy.username:
|
||||
socks_url = f"socks5://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
|
||||
else:
|
||||
socks_url = f"socks5://{settings.proxy.host}:{settings.proxy.port}"
|
||||
transport = AsyncProxyTransport.from_url(socks_url, rdns=True)
|
||||
_shared_client = httpx.AsyncClient(transport=transport, timeout=timeout)
|
||||
else:
|
||||
_shared_client = httpx.AsyncClient(timeout=timeout)
|
||||
else:
|
||||
_shared_client = httpx.AsyncClient(timeout=timeout)
|
||||
_shared_client_proxy_key = current_key
|
||||
return _shared_client
|
||||
|
||||
|
||||
class RequestURL:
|
||||
def __init__(self):
|
||||
@@ -86,31 +126,9 @@ class RequestURL:
|
||||
return None
|
||||
|
||||
async def __aenter__(self):
|
||||
timeout = httpx.Timeout(connect=5.0, read=10.0, write=10.0, pool=10.0)
|
||||
if settings.proxy.enable:
|
||||
if "http" in settings.proxy.type:
|
||||
if settings.proxy.username:
|
||||
username = settings.proxy.username
|
||||
password = settings.proxy.password
|
||||
proxy_url = f"http://{username}:{password}@{settings.proxy.host}:{settings.proxy.port}"
|
||||
else:
|
||||
proxy_url = f"http://{settings.proxy.host}:{settings.proxy.port}"
|
||||
self._client = httpx.AsyncClient(proxy=proxy_url, timeout=timeout)
|
||||
elif settings.proxy.type == "socks5":
|
||||
if settings.proxy.username:
|
||||
socks_url = f"socks5://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
|
||||
else:
|
||||
socks_url = f"socks5://{settings.proxy.host}:{settings.proxy.port}"
|
||||
transport = AsyncProxyTransport.from_url(socks_url, rdns=True)
|
||||
self._client = httpx.AsyncClient(transport=transport, timeout=timeout)
|
||||
else:
|
||||
logger.error(f"[Network] Unsupported proxy type: {settings.proxy.type}")
|
||||
self._client = httpx.AsyncClient(timeout=timeout)
|
||||
else:
|
||||
self._client = httpx.AsyncClient(timeout=timeout)
|
||||
self._client = await get_shared_client()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
# Client is shared; do not close it here
|
||||
self._client = None
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
def rss_parser(soup):
|
||||
torrent_titles = []
|
||||
torrent_urls = []
|
||||
torrent_homepage = []
|
||||
results = []
|
||||
for item in soup.findall("./channel/item"):
|
||||
torrent_titles.append(item.find("title").text)
|
||||
title = item.find("title").text
|
||||
enclosure = item.find("enclosure")
|
||||
if enclosure is not None:
|
||||
torrent_homepage.append(item.find("link").text)
|
||||
torrent_urls.append(enclosure.attrib.get("url"))
|
||||
homepage = item.find("link").text
|
||||
url = enclosure.attrib.get("url")
|
||||
else:
|
||||
torrent_urls.append(item.find("link").text)
|
||||
torrent_homepage.append("")
|
||||
return torrent_titles, torrent_urls, torrent_homepage
|
||||
url = item.find("link").text
|
||||
homepage = ""
|
||||
results.append((title, url, homepage))
|
||||
return results
|
||||
|
||||
|
||||
def mikan_title(soup):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from module.conf import settings
|
||||
@@ -35,13 +36,13 @@ class PostNotification:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_poster(notify: Notification):
|
||||
def _get_poster_sync(notify: Notification):
|
||||
with Database() as db:
|
||||
poster_path = db.bangumi.match_poster(notify.official_title)
|
||||
notify.poster_path = poster_path
|
||||
|
||||
async def send_msg(self, notify: Notification) -> bool:
|
||||
self._get_poster(notify)
|
||||
await asyncio.to_thread(self._get_poster_sync, notify)
|
||||
try:
|
||||
await self.notifier.post_msg(notify)
|
||||
logger.debug(f"Send notification: {notify.official_title}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -6,8 +7,16 @@ from urllib3.util import parse_url
|
||||
from module.network import RequestContent
|
||||
from module.utils import save_image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# In-memory cache for Mikan homepage lookups
|
||||
_mikan_cache: dict[str, tuple[str, str]] = {}
|
||||
|
||||
|
||||
async def mikan_parser(homepage: str):
|
||||
if homepage in _mikan_cache:
|
||||
logger.debug(f"[Mikan] Cache hit for {homepage}")
|
||||
return _mikan_cache[homepage]
|
||||
root_path = parse_url(homepage).host
|
||||
async with RequestContent() as req:
|
||||
content = await req.get_html(homepage)
|
||||
@@ -23,8 +32,12 @@ async def mikan_parser(homepage: str):
|
||||
img = await req.get_content(f"https://{root_path}{poster_path}")
|
||||
suffix = poster_path.split(".")[-1]
|
||||
poster_link = save_image(img, suffix)
|
||||
return poster_link, official_title
|
||||
return "", ""
|
||||
result = (poster_link, official_title)
|
||||
_mikan_cache[homepage] = result
|
||||
return result
|
||||
result = ("", "")
|
||||
_mikan_cache[homepage] = result
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -6,8 +7,13 @@ from module.conf import TMDB_API
|
||||
from module.network import RequestContent
|
||||
from module.utils import save_image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TMDB_URL = "https://api.themoviedb.org"
|
||||
|
||||
# In-memory cache for TMDB lookups to avoid repeated API calls
|
||||
_tmdb_cache: dict[str, "TMDBInfo | None"] = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TMDBInfo:
|
||||
@@ -57,6 +63,11 @@ def get_season(seasons: list) -> tuple[int, str]:
|
||||
|
||||
|
||||
async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
|
||||
cache_key = f"{title}:{language}"
|
||||
if cache_key in _tmdb_cache:
|
||||
logger.debug(f"[TMDB] Cache hit for {title}")
|
||||
return _tmdb_cache[cache_key]
|
||||
|
||||
async with RequestContent() as req:
|
||||
url = search_url(title)
|
||||
contents = await req.get_json(url)
|
||||
@@ -99,7 +110,7 @@ async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
|
||||
poster_link = "https://image.tmdb.org/t/p/w780" + poster_path
|
||||
else:
|
||||
poster_link = None
|
||||
return TMDBInfo(
|
||||
result = TMDBInfo(
|
||||
id,
|
||||
official_title,
|
||||
original_title,
|
||||
@@ -108,7 +119,10 @@ async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
|
||||
str(year_number),
|
||||
poster_link,
|
||||
)
|
||||
_tmdb_cache[cache_key] = result
|
||||
return result
|
||||
else:
|
||||
_tmdb_cache[cache_key] = None
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ RULES = [
|
||||
r"(.*)(?:S\d{2})?EP?(\d{1,4}(?:\.\d{1,2})?)(.*)",
|
||||
]
|
||||
|
||||
COMPILED_RULES = [re.compile(rule, re.I) for rule in RULES]
|
||||
|
||||
SUBTITLE_LANG = {
|
||||
"zh-tw": ["tc", "cht", "繁", "zh-tw"],
|
||||
"zh": ["sc", "chs", "简", "zh"],
|
||||
@@ -34,10 +36,11 @@ def get_path_basename(torrent_path: str) -> str:
|
||||
return Path(torrent_path).name
|
||||
|
||||
|
||||
_GROUP_SPLIT_RE = re.compile(r"[\[\]()【】()]")
|
||||
|
||||
|
||||
def get_group(group_and_title) -> tuple[str | None, str]:
|
||||
n = re.split(r"[\[\]()【】()]", group_and_title)
|
||||
while "" in n:
|
||||
n.remove("")
|
||||
n = [x for x in _GROUP_SPLIT_RE.split(group_and_title) if x]
|
||||
if len(n) > 1:
|
||||
if re.match(r"\d+", n[1]):
|
||||
return None, group_and_title
|
||||
@@ -73,8 +76,8 @@ def torrent_parser(
|
||||
if torrent_name is None:
|
||||
match_names = match_names[1:]
|
||||
for match_name in match_names:
|
||||
for rule in RULES:
|
||||
match_obj = re.match(rule, match_name, re.I)
|
||||
for compiled_rule in COMPILED_RULES:
|
||||
match_obj = compiled_rule.match(match_name)
|
||||
if match_obj:
|
||||
group, title = get_group(match_obj.group(1))
|
||||
if not season:
|
||||
|
||||
@@ -46,12 +46,14 @@ class RSSAnalyser(TitleParser):
|
||||
self, torrents: list[Torrent], rss: RSSItem, full_parse: bool = True
|
||||
) -> list:
|
||||
new_data = []
|
||||
seen_titles: set[str] = set()
|
||||
for torrent in torrents:
|
||||
bangumi = self.raw_parser(raw=torrent.name)
|
||||
if bangumi and bangumi.title_raw not in [i.title_raw for i in new_data]:
|
||||
if bangumi and bangumi.title_raw not in seen_titles:
|
||||
await self.official_title_parser(bangumi=bangumi, rss=rss, torrent=torrent)
|
||||
if not full_parse:
|
||||
return [bangumi]
|
||||
seen_titles.add(bangumi.title_raw)
|
||||
new_data.append(bangumi)
|
||||
logger.info(f"[RSS] New bangumi founded: {bangumi.official_title}")
|
||||
return new_data
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
@@ -65,8 +66,7 @@ class RSSEngine(Database):
|
||||
)
|
||||
|
||||
def disable_list(self, rss_id_list: list[int]):
|
||||
for rss_id in rss_id_list:
|
||||
self.rss.disable(rss_id)
|
||||
self.rss.disable_batch(rss_id_list)
|
||||
return ResponseModel(
|
||||
status=True,
|
||||
status_code=200,
|
||||
@@ -75,8 +75,7 @@ class RSSEngine(Database):
|
||||
)
|
||||
|
||||
def enable_list(self, rss_id_list: list[int]):
|
||||
for rss_id in rss_id_list:
|
||||
self.rss.enable(rss_id)
|
||||
self.rss.enable_batch(rss_id_list)
|
||||
return ResponseModel(
|
||||
status=True,
|
||||
status_code=200,
|
||||
@@ -99,13 +98,22 @@ class RSSEngine(Database):
|
||||
new_torrents = self.torrent.check_new(torrents)
|
||||
return new_torrents
|
||||
|
||||
_filter_cache: dict[str, re.Pattern] = {}
|
||||
|
||||
def _get_filter_pattern(self, filter_str: str) -> re.Pattern:
|
||||
if filter_str not in self._filter_cache:
|
||||
self._filter_cache[filter_str] = re.compile(
|
||||
filter_str.replace(",", "|"), re.IGNORECASE
|
||||
)
|
||||
return self._filter_cache[filter_str]
|
||||
|
||||
def match_torrent(self, torrent: Torrent) -> Optional[Bangumi]:
|
||||
matched: Bangumi = self.bangumi.match_torrent(torrent.name)
|
||||
if matched:
|
||||
if matched.filter == "":
|
||||
return matched
|
||||
_filter = matched.filter.replace(",", "|")
|
||||
if not re.search(_filter, torrent.name, re.IGNORECASE):
|
||||
pattern = self._get_filter_pattern(matched.filter)
|
||||
if not pattern.search(torrent.name):
|
||||
torrent.bangumi_id = matched.id
|
||||
return matched
|
||||
return None
|
||||
@@ -117,11 +125,13 @@ class RSSEngine(Database):
|
||||
else:
|
||||
rss_item = self.rss.search_id(rss_id)
|
||||
rss_items = [rss_item] if rss_item else []
|
||||
# From RSS Items, get all torrents
|
||||
# From RSS Items, fetch all torrents concurrently
|
||||
logger.debug(f"[Engine] Get {len(rss_items)} RSS items")
|
||||
for rss_item in rss_items:
|
||||
new_torrents = await self.pull_rss(rss_item)
|
||||
# Get all enabled bangumi data
|
||||
all_torrents = await asyncio.gather(
|
||||
*[self.pull_rss(rss_item) for rss_item in rss_items]
|
||||
)
|
||||
# Process results sequentially (DB operations)
|
||||
for rss_item, new_torrents in zip(rss_items, all_torrents):
|
||||
for torrent in new_torrents:
|
||||
matched_data = self.match_torrent(torrent)
|
||||
if matched_data:
|
||||
|
||||
Reference in New Issue
Block a user