Skip to content

Commit c0c1f42

Browse files
authored
Merge pull request #92 from shinonomeow/shino_aio
add time limit for torrent monitor, fix dmhy hash parser
2 parents e6ecfcc + 13d10f2 commit c0c1f42

6 files changed

Lines changed: 153 additions & 31 deletions

File tree

backend/src/module/core/renamer_service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from typing import Any
3+
import asyncio
34

45
from module.conf import settings
56
from module.core.services import BaseService
@@ -26,7 +27,6 @@ async def initialize(self) -> None:
2627
await super().initialize()
2728
self.enable = settings.bangumi_manage.enable
2829

29-
3030
def get_task_config(self) -> dict[str, Any]:
3131
"""获取重命名任务配置"""
3232
return {
@@ -89,7 +89,9 @@ async def _publish_download_completed_event(self, torrent) -> None:
8989
},
9090
)
9191

92-
await event_bus.publish(event)
92+
asyncio.create_task(
93+
event_bus.publish(event) # 异步执行事件发布
94+
) # 异步执行重命名
9395
logger.debug(f"[RenamerService] 已发布重命名事件: {torrent.name}")
9496

9597
except Exception as e:

backend/src/module/downloader/client/qbittorrent/qbittorrent.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from module.conf import get_plugin_config
77
from module.models import TorrentDownloadInfo
88
from module.network import RequestContent
9+
from module.utils import get_hash, base32_to_hex
910

1011
from ....conf import settings
1112
from ..base_downloader import BaseDownloader
@@ -230,14 +231,30 @@ async def add(self, torrent_url, save_path, category) -> str | None:
230231
)
231232
torrent_hashes = await req.get_torrent_hash(torrent_url)
232233
# 优先使用v2 hash,如果没有则使用v1 hash
233-
torrent_link = torrent_hashes.get("v2", torrent_hashes.get("v1", ""))
234+
torrent_link = torrent_hashes.get(
235+
"v2", torrent_hashes.get("v1", "")
236+
)
234237
logger.debug(f"[QbDownloader] Got torrent hashes: {torrent_hashes}")
235238
logger.debug(f"[QbDownloader] Using hash: {torrent_link}")
236239
file = {"torrents": torrent_file}
237240
else:
238241
logger.warning(
239242
f"[QbDownloader] Failed to get torrent content from {torrent_url}"
240243
)
244+
else:
245+
# 如果是 magnet 链接,直接使用
246+
torrent_link = get_hash(torrent_url)
247+
if torrent_link:
248+
# 判断是否为32字符的Base32格式(DMHY等站点使用)
249+
if len(torrent_link) == 32:
250+
# 转换Base32格式为40字符小写十六进制
251+
hex_hash = base32_to_hex(torrent_link)
252+
if hex_hash:
253+
torrent_link = hex_hash
254+
logger.debug(
255+
f"[QbDownloader] 转换Base32 hash为十六进制: {torrent_link}"
256+
)
257+
logger.debug(f"[QbDownloader] Using magnet link: {torrent_url}")
241258
try:
242259
resp = await self._client.post(
243260
url=QB_API_URL["add"],

backend/src/module/downloader/download_monitor.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
from datetime import datetime, timedelta
34

45
from module.database import Database
56
from module.downloader.download_client import Client as download_client
@@ -108,13 +109,37 @@ async def monitor_torrent(self, bangumi: Bangumi, torrent: Torrent) -> None:
108109
"""
109110
try:
110111
torrent_hash = torrent.download_uid
111-
logger.debug(f"[DownloadMonitor] 开始监控种子: {torrent.name} ({torrent_hash})")
112+
logger.debug(
113+
f"[DownloadMonitor] 开始监控种子: {torrent.name} ({torrent_hash})"
114+
)
112115
if not torrent_hash:
113116
logger.warning(f"[DownloadMonitor] 种子 {torrent.name} 没有下载UID")
114117
return
115118

116119
retry_count = 0
120+
start_time = datetime.now()
121+
timeout_limit = timedelta(hours=4)
122+
117123
while not self._shutdown:
124+
# 检查是否超过4小时超时限制
125+
current_time = datetime.now()
126+
if current_time - start_time > timeout_limit:
127+
logger.warning(
128+
f"[DownloadMonitor] 种子 {torrent.name} 超过4小时未完成下载,标记为已下载和已重命名"
129+
)
130+
try:
131+
with Database() as db:
132+
if torrent_item := db.torrent.search_by_duid(torrent_hash):
133+
torrent_item.downloaded = True
134+
torrent_item.renamed = True
135+
db.torrent.add(torrent_item)
136+
logger.info(
137+
f"[DownloadMonitor] 已将超时种子标记为完成: {torrent.name}"
138+
)
139+
except Exception as e:
140+
logger.error(f"[DownloadMonitor] 更新超时种子状态失败: {e}")
141+
break
142+
118143
# 获取种子信息
119144
info = await download_client.get_torrent_info(torrent_hash)
120145
logger.debug(

backend/src/module/utils/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from .bangumi_data import get_hash
21
from .cache_image import gen_poster_path, str_to_url, url_to_str
32
from .events import Event, EventBus, EventType, event_bus
43
from .path_parser import check_file, gen_save_path, get_path_basename, path_to_bangumi
5-
from .torrent import torrent_to_link, get_torrent_hashes
4+
from .torrent import torrent_to_link, get_torrent_hashes, normalize_hash, base32_to_hex, hex_to_base32, get_hash
65

76
__all__ = [
87
"check_file",
@@ -11,6 +10,9 @@
1110
"url_to_str",
1211
"torrent_to_link",
1312
"get_torrent_hashes",
13+
"normalize_hash",
14+
"base32_to_hex",
15+
"hex_to_base32",
1416
"str_to_url",
1517
"path_to_bangumi",
1618
"get_hash",

backend/src/module/utils/bangumi_data.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

backend/src/module/utils/torrent.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import hashlib
33
from urllib.parse import quote
44
import logging
5+
import re
56

67
import bencodepy
78

@@ -119,3 +120,103 @@ async def get_torrent_hashes(torrent_file):
119120
except Exception as e:
120121
logger.error(f"get_torrent_hashes: Error processing torrent file: {e}")
121122
return {"v1": ""}
123+
124+
125+
def base32_to_hex(base32_hash: str) -> str:
126+
"""将Base32格式的hash转换为十六进制
127+
128+
Args:
129+
base32_hash: 32字符的Base32编码hash
130+
131+
Returns:
132+
40字符的小写十六进制hash,转换失败返回空字符串
133+
"""
134+
try:
135+
# Base32解码
136+
decoded = base64.b32decode(base32_hash)
137+
return decoded.hex().lower()
138+
except Exception as e:
139+
logger.warning(f"Base32转换失败: {base32_hash}, error: {e}")
140+
return ""
141+
142+
143+
def hex_to_base32(hex_hash: str) -> str:
144+
"""将十六进制hash转换为Base32格式
145+
146+
Args:
147+
hex_hash: 40字符的十六进制hash
148+
149+
Returns:
150+
32字符的Base32编码hash,转换失败返回空字符串
151+
"""
152+
try:
153+
# 十六进制转字节
154+
hex_bytes = bytes.fromhex(hex_hash)
155+
# 编码为Base32,去掉填充符
156+
return base64.b32encode(hex_bytes).decode().rstrip('=')
157+
except Exception as e:
158+
logger.warning(f"十六进制转Base32失败: {hex_hash}, error: {e}")
159+
return ""
160+
161+
162+
def normalize_hash(hash_value: str) -> str:
163+
"""标准化hash格式,统一转换为40字符小写十六进制
164+
165+
Args:
166+
hash_value: 输入的hash值,可能是40字符十六进制或32字符Base32
167+
168+
Returns:
169+
标准化的40字符小写十六进制hash
170+
"""
171+
if not hash_value:
172+
return ""
173+
174+
hash_value = hash_value.strip()
175+
176+
# 40字符十六进制格式
177+
if len(hash_value) == 40 and re.match(r'^[a-fA-F0-9]{40}$', hash_value):
178+
return hash_value.lower()
179+
180+
# 32字符Base32格式 (DMHY等站点常用)
181+
if len(hash_value) == 32 and re.match(r'^[A-Z0-9]{32}$', hash_value):
182+
hex_hash = base32_to_hex(hash_value)
183+
if hex_hash:
184+
logger.debug(f"将Base32 hash {hash_value} 转换为十六进制: {hex_hash}")
185+
return hex_hash
186+
187+
# 其他情况,尝试转小写
188+
normalized = hash_value.lower()
189+
logger.debug(f"hash标准化: {hash_value} -> {normalized}")
190+
return normalized
191+
192+
193+
def get_hash(torrent_url: str) -> str | None:
194+
"""从torrent URL或magnet链接中提取hash
195+
196+
Args:
197+
torrent_url: torrent文件URL或magnet链接
198+
199+
Returns:
200+
提取的hash值,提取失败返回空字符串
201+
"""
202+
hash_pattern_dict = {
203+
"magnet_hash_pattern": re.compile(r"\b([a-fA-F0-9]{40})\b"),
204+
"torrent_hash_pattern": re.compile(r"/([a-fA-F0-9]{7,40})\.torrent"),
205+
"dmhy_hash_pattern": re.compile(r"urn:btih:([A-Z0-9]{32})"),
206+
}
207+
208+
for pattern_name, hash_pattern in hash_pattern_dict.items():
209+
ans = re.search(hash_pattern, torrent_url)
210+
if ans:
211+
extracted_hash = ans[1]
212+
logger.debug(f"使用{pattern_name}提取hash: {extracted_hash}")
213+
214+
# 使用normalize_hash标准化hash格式
215+
normalized_hash = normalize_hash(extracted_hash)
216+
if normalized_hash != extracted_hash:
217+
logger.debug(f"hash已标准化: {extracted_hash} -> {normalized_hash}")
218+
219+
return normalized_hash
220+
221+
logger.warning(f"[Utils] Cannot find hash in {torrent_url}")
222+
return ""

0 commit comments

Comments
 (0)