Skip to content

Commit da6e578

Browse files
EstrellaXDclaude
andcommitted
fix(network): improve torrent fetch reliability and error handling
- Add browser-like headers and full Chrome User-Agent to avoid Cloudflare blocking - Use appropriate Accept headers for torrent files (application/x-bittorrent) - Increase timeouts (connect: 5s→10s, read: 10s→30s) for slow responses - Filter out None values from failed torrent fetches before sending to qBittorrent - Add try-catch around add_torrents to prevent request crashes - Improve logging from DEBUG to WARNING level for better visibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6ce3f92 commit da6e578

3 files changed

Lines changed: 57 additions & 19 deletions

File tree

backend/src/module/downloader/download_client.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def __getClient():
3131
from .client.aria2_downloader import Aria2Downloader
3232

3333
return Aria2Downloader(host, username, password)
34+
elif type == "mock":
35+
from .client.mock_downloader import MockDownloader
36+
37+
logger.info("[Downloader] Using MockDownloader for local development")
38+
return MockDownloader()
3439
else:
3540
logger.error(f"[Downloader] Unsupported downloader type: {type}")
3641
raise Exception(f"Unsupported downloader type: {type}")
@@ -141,24 +146,36 @@ async def add_torrent(self, torrent: Torrent | list, bangumi: Bangumi) -> bool:
141146
torrent_file = await asyncio.gather(
142147
*[req.get_content(t.url) for t in torrent]
143148
)
149+
# Filter out None values (failed fetches)
150+
torrent_file = [f for f in torrent_file if f is not None]
151+
if not torrent_file:
152+
logger.warning(f"[Downloader] Failed to fetch torrent files for: {bangumi.official_title}")
153+
return False
144154
torrent_url = None
145155
else:
146156
if "magnet" in torrent.url:
147157
torrent_url = torrent.url
148158
torrent_file = None
149159
else:
150160
torrent_file = await req.get_content(torrent.url)
161+
if torrent_file is None:
162+
logger.warning(f"[Downloader] Failed to fetch torrent file for: {bangumi.official_title}")
163+
return False
151164
torrent_url = None
152-
if await self.client.add_torrents(
153-
torrent_urls=torrent_url,
154-
torrent_files=torrent_file,
155-
save_path=bangumi.save_path,
156-
category="Bangumi",
157-
):
158-
logger.debug(f"[Downloader] Add torrent: {bangumi.official_title}")
159-
return True
160-
else:
161-
logger.debug(f"[Downloader] Torrent added before: {bangumi.official_title}")
165+
try:
166+
if await self.client.add_torrents(
167+
torrent_urls=torrent_url,
168+
torrent_files=torrent_file,
169+
save_path=bangumi.save_path,
170+
category="Bangumi",
171+
):
172+
logger.debug(f"[Downloader] Add torrent: {bangumi.official_title}")
173+
return True
174+
else:
175+
logger.debug(f"[Downloader] Torrent added before: {bangumi.official_title}")
176+
return False
177+
except Exception as e:
178+
logger.error(f"[Downloader] Failed to add torrent for {bangumi.official_title}: {e}")
162179
return False
163180

164181
async def move_torrent(self, hashes, location):

backend/src/module/network/request_contents.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ async def get_content(self, _url):
6767
req = await self.get_url(_url)
6868
if req:
6969
return req.content
70+
logger.warning(f"[Network] Failed to get content from {_url}")
71+
return None
7072

7173
async def check_connection(self, _url):
7274
return await self.check_url(_url)

backend/src/module/network/request_url.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def get_shared_client() -> httpx.AsyncClient:
2626
return _shared_client
2727
if _shared_client is not None:
2828
await _shared_client.aclose()
29-
timeout = httpx.Timeout(connect=5.0, read=10.0, write=10.0, pool=10.0)
29+
timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0)
3030
if settings.proxy.enable:
3131
if "http" in settings.proxy.type:
3232
if settings.proxy.username:
@@ -50,31 +50,50 @@ async def get_shared_client() -> httpx.AsyncClient:
5050

5151

5252
class RequestURL:
53+
# More complete User-Agent to avoid Cloudflare blocking
54+
DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
55+
5356
def __init__(self):
54-
self.header = {"user-agent": "Mozilla/5.0", "Accept": "application/xml"}
57+
self.header = {"User-Agent": self.DEFAULT_UA, "Accept": "application/xml"}
5558
self._client: httpx.AsyncClient | None = None
5659

60+
def _get_headers(self, url: str) -> dict:
61+
"""Get appropriate headers based on URL type."""
62+
base_headers = {
63+
"User-Agent": self.DEFAULT_UA,
64+
"Accept-Language": "en-US,en;q=0.9",
65+
"Accept-Encoding": "gzip, deflate, br",
66+
"Connection": "keep-alive",
67+
}
68+
# For torrent files, use different Accept header
69+
if url.endswith(".torrent") or "/download/" in url:
70+
base_headers["Accept"] = "application/x-bittorrent, application/octet-stream, */*"
71+
else:
72+
base_headers["Accept"] = "application/xml, text/xml, */*"
73+
return base_headers
74+
5775
async def get_url(self, url, retry=3):
5876
try_time = 0
77+
headers = self._get_headers(url)
5978
while True:
6079
try:
61-
req = await self._client.get(url=url, headers=self.header)
80+
req = await self._client.get(url=url, headers=headers)
6281
logger.debug(f"[Network] Successfully connected to {url}. Status: {req.status_code}")
6382
req.raise_for_status()
6483
return req
65-
except httpx.HTTPStatusError:
66-
logger.debug(f"[Network] HTTP error from {url}.")
84+
except httpx.HTTPStatusError as e:
85+
logger.warning(f"[Network] HTTP {e.response.status_code} from {url}")
6786
break
68-
except httpx.RequestError:
69-
logger.debug(
70-
f"[Network] Cannot connect to {url}. Wait for 5 seconds."
87+
except httpx.RequestError as e:
88+
logger.warning(
89+
f"[Network] Request error for {url}: {type(e).__name__}. Retry {try_time + 1}/{retry}"
7190
)
7291
try_time += 1
7392
if try_time >= retry:
7493
break
7594
await asyncio.sleep(5)
7695
except Exception as e:
77-
logger.debug(e)
96+
logger.warning(f"[Network] Unexpected error for {url}: {e}")
7897
break
7998
logger.error(f"[Network] Unable to connect to {url}, Please check your network settings")
8099
return None

0 commit comments

Comments
 (0)