Skip to content

Commit 1fbc0e3

Browse files
authored
Merge pull request #1027 from EstrellaXD/fix/batch-3.2.7-followup
fix: batch bug fixes for 3.2.7 (#1016, #983, #1025, #994, #1015)
2 parents d849edf + 07522ae commit 1fbc0e3

7 files changed

Lines changed: 115 additions & 27 deletions

File tree

.github/workflows/build.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,18 @@ jobs:
314314
echo ${{ needs.version-info.outputs.version }}
315315
echo "VERSION='${{ needs.version-info.outputs.version }}'" >> module/__version__.py
316316
317+
- uses: astral-sh/setup-uv@v4
318+
with:
319+
version: "latest"
320+
321+
- name: Generate requirements.txt for non-uv consumers (#994)
322+
run: |
323+
cd backend && uv export --format requirements-txt --no-hashes --no-dev -o requirements.txt
324+
317325
- name: Zip app
318326
run: |
319-
cd backend && zip -r app-v${{ needs.version-info.outputs.version }}.zip src
327+
cd backend && zip -r app-v${{ needs.version-info.outputs.version }}.zip \
328+
src pyproject.toml uv.lock requirements.txt
320329
321330
- name: Generate Release info
322331
id: release-info

backend/src/module/downloader/path.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import re
33
from os import PathLike
4+
from pathlib import PureWindowsPath
45

56
from module.conf import PLATFORM, settings
67
from module.models import Bangumi, BangumiUpdate
@@ -36,9 +37,11 @@ def check_files(files: list[dict]):
3637

3738
@staticmethod
3839
def _path_to_bangumi(save_path: PathLike[str] | str, torrent_name: str = ""):
39-
# Split save path and download path
40-
save_parts = Path(save_path).parts
41-
download_parts = Path(settings.downloader.path).parts
40+
# Use PureWindowsPath regardless of the host AB runs on: it accepts
41+
# both "\" and "/" separators, so a qBittorrent-on-Windows save_path
42+
# reaching a Linux AB still splits into segments correctly (#1016).
43+
save_parts = PureWindowsPath(save_path).parts
44+
download_parts = PureWindowsPath(settings.downloader.path).parts
4245
# Get bangumi name and season
4346
bangumi_name = ""
4447
season = 1

backend/src/module/network/request_url.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,32 @@ async def get_shared_client() -> httpx.AsyncClient:
3636
if _shared_client is not None:
3737
await _shared_client.aclose()
3838
timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0)
39+
# follow_redirects=True: Mikan mirrors and some CDNs respond with 302 to the
40+
# canonical host; without this, raise_for_status treats the redirect as an
41+
# error and the RSS pull fails (#983).
42+
common_kwargs = {
43+
"timeout": timeout,
44+
"limits": _CONNECTION_LIMITS,
45+
"follow_redirects": True,
46+
}
3947
if settings.proxy.enable:
4048
if "http" in settings.proxy.type:
4149
if settings.proxy.username:
4250
proxy_url = f"http://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
4351
else:
4452
proxy_url = f"http://{settings.proxy.host}:{settings.proxy.port}"
45-
_shared_client = httpx.AsyncClient(
46-
proxy=proxy_url, timeout=timeout, limits=_CONNECTION_LIMITS
47-
)
53+
_shared_client = httpx.AsyncClient(proxy=proxy_url, **common_kwargs)
4854
elif settings.proxy.type == "socks5":
4955
if settings.proxy.username:
5056
socks_url = f"socks5://{settings.proxy.username}:{settings.proxy.password}@{settings.proxy.host}:{settings.proxy.port}"
5157
else:
5258
socks_url = f"socks5://{settings.proxy.host}:{settings.proxy.port}"
5359
transport = AsyncProxyTransport.from_url(socks_url, rdns=True)
54-
_shared_client = httpx.AsyncClient(
55-
transport=transport, timeout=timeout, limits=_CONNECTION_LIMITS
56-
)
60+
_shared_client = httpx.AsyncClient(transport=transport, **common_kwargs)
5761
else:
58-
_shared_client = httpx.AsyncClient(
59-
timeout=timeout, limits=_CONNECTION_LIMITS
60-
)
62+
_shared_client = httpx.AsyncClient(**common_kwargs)
6163
else:
62-
_shared_client = httpx.AsyncClient(timeout=timeout, limits=_CONNECTION_LIMITS)
64+
_shared_client = httpx.AsyncClient(**common_kwargs)
6365
_shared_client_proxy_key = current_key
6466
return _shared_client
6567

@@ -91,7 +93,9 @@ def _get_headers(self, url: str) -> dict:
9193
}
9294
# For torrent files, use different Accept header
9395
if url.endswith(".torrent") or "/download/" in url:
94-
base_headers["Accept"] = "application/x-bittorrent, application/octet-stream, */*"
96+
base_headers["Accept"] = (
97+
"application/x-bittorrent, application/octet-stream, */*"
98+
)
9599
else:
96100
base_headers["Accept"] = "application/xml, text/xml, */*"
97101
return base_headers
@@ -102,7 +106,11 @@ async def get_url(self, url, retry=3):
102106
while True:
103107
try:
104108
req = await self._client.get(url=url, headers=headers)
105-
logger.debug("[Network] Successfully connected to %s. Status: %s", url, req.status_code)
109+
logger.debug(
110+
"[Network] Successfully connected to %s. Status: %s",
111+
url,
112+
req.status_code,
113+
)
106114
req.raise_for_status()
107115
return req
108116
except httpx.HTTPStatusError as e:
@@ -122,16 +130,16 @@ async def get_url(self, url, retry=3):
122130
except Exception as e:
123131
logger.warning(f"[Network] Unexpected error for {url}: {e}")
124132
break
125-
logger.error(f"[Network] Unable to connect to {url}, Please check your network settings")
133+
logger.error(
134+
f"[Network] Unable to connect to {url}, Please check your network settings"
135+
)
126136
return None
127137

128138
async def post_url(self, url: str, data: dict, retry=3):
129139
try_time = 0
130140
while True:
131141
try:
132-
req = await self._client.post(
133-
url=url, headers=self.header, data=data
134-
)
142+
req = await self._client.post(url=url, headers=self.header, data=data)
135143
req.raise_for_status()
136144
return req
137145
except httpx.RequestError:

backend/src/module/parser/analyser/raw_parser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ def pre_process(raw_name: str) -> str:
5959

6060

6161
def prefix_process(raw: str, group: str) -> str:
62-
raw = re.sub(f".{re.escape(group)}.", "", raw)
62+
# Guard against empty group: without this, the pattern degenerates to ".."
63+
# and every pair of characters gets deleted, destroying titles that lack a
64+
# [group] prefix (#1025).
65+
if group:
66+
raw = re.sub(f".{re.escape(group)}.", "", raw)
6367
raw_process = PREFIX_RE.sub("/", raw)
6468
arg_group = raw_process.split("/")
6569
while "" in arg_group:

backend/src/test/test_path_parser.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ def test_path_to_bangumi():
1313
assert season == 2
1414

1515

16+
def test_path_to_bangumi_windows_style_save_path():
17+
"""Regression for #1016: when qBittorrent runs on Windows and AB runs on
18+
Linux, qB returns backslash paths. PurePosixPath treats the whole string
19+
as one segment, leaving season stuck at 1."""
20+
from module.downloader.path import TorrentPath
21+
22+
with patch("module.downloader.path.settings") as mock_settings:
23+
mock_settings.downloader.path = r"D:\video\Bangumis"
24+
path = r"D:\video\Bangumis\小书痴的下克上\Season 4"
25+
bangumi_name, season = TorrentPath._path_to_bangumi(path)
26+
27+
assert bangumi_name == "小书痴的下克上"
28+
assert season == 4
29+
30+
31+
def test_path_to_bangumi_posix_path_on_linux_ab():
32+
"""Regression guard: POSIX paths still parse correctly after the fix."""
33+
from module.downloader.path import TorrentPath
34+
35+
with patch("module.downloader.path.settings") as mock_settings:
36+
mock_settings.downloader.path = "/downloads/Bangumi"
37+
path = "/downloads/Bangumi/葬送的芙莉莲/Season 2"
38+
bangumi_name, season = TorrentPath._path_to_bangumi(path)
39+
40+
assert bangumi_name == "葬送的芙莉莲"
41+
assert season == 2
42+
43+
1644
class TestGenSavePath:
1745
"""Tests for TorrentPath._gen_save_path with season_offset."""
1846

backend/src/test/test_raw_parser.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def test_raw_parser():
5656
assert info.episode == 9
5757
assert info.season == 1
5858

59-
content = "[梦蓝字幕组]New Doraemon 哆啦A梦新番[747][2023.02.25][AVC][1080P][GB_JP][MP4]"
59+
content = (
60+
"[梦蓝字幕组]New Doraemon 哆啦A梦新番[747][2023.02.25][AVC][1080P][GB_JP][MP4]"
61+
)
6062
info = raw_parser(content)
6163
assert info.group == "梦蓝字幕组"
6264
assert info.title_zh == "哆啦A梦新番"
@@ -65,7 +67,9 @@ def test_raw_parser():
6567
assert info.episode == 747
6668
assert info.season == 1
6769

68-
content = "[织梦字幕组][尼尔:机械纪元 NieR Automata Ver1.1a][02集][1080P][AVC][简日双语]"
70+
content = (
71+
"[织梦字幕组][尼尔:机械纪元 NieR Automata Ver1.1a][02集][1080P][AVC][简日双语]"
72+
)
6973
info = raw_parser(content)
7074
assert info.group == "织梦字幕组"
7175
assert info.title_zh == "尼尔:机械纪元"
@@ -160,7 +164,9 @@ def test_raw_parser():
160164
assert info.season == 1
161165

162166
# Issue #990: Title starting with number — should not misparse "29" as episode
163-
content = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"
167+
content = (
168+
"[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"
169+
)
164170
info = raw_parser(content)
165171
assert info.group == "ANi"
166172
assert info.title_zh == "29 岁单身中坚冒险家的日常"
@@ -310,8 +316,9 @@ def test_parse_western_format(self):
310316
assert info.resolution == "1080p"
311317
# No brackets → group detection fails
312318
assert info.group == ""
313-
# No CJK chars → no title_zh/jp; EN detection also fails (short segments)
314-
assert info.title_en is None
319+
# After the #1025 fix, prefix_process no longer destroys titles without
320+
# a [group] prefix, so the English title is now extracted correctly.
321+
assert info.title_en == "Girls Band Cry"
315322
assert info.title_zh is None
316323

317324

@@ -323,7 +330,9 @@ class TestIssue986AtlasFormat:
323330
"[阿特拉斯字幕组·雪原市出差所][命运-奇异赝品_Fate/strange Fake][07_神自黄昏归来][简繁日内封PGS][日语配音版_Japanese Dub][Web-DL Remux][1080p AVC AAC]",
324331
]
325332

326-
@pytest.mark.xfail(reason="Atlas bracket-delimited format not supported by TITLE_RE")
333+
@pytest.mark.xfail(
334+
reason="Atlas bracket-delimited format not supported by TITLE_RE"
335+
)
327336
def test_parse_atlas_format(self):
328337
info = raw_parser(self.TITLES[0])
329338
assert info is not None
@@ -362,3 +371,24 @@ def test_parse_cht_title(self):
362371
assert info.source == "Baha"
363372
assert info.sub == "CHT"
364373

374+
375+
class TestIssue1025NoGroupPrefix:
376+
"""Issue #1025: Titles without a [group] prefix must still parse.
377+
378+
prefix_process was calling re.sub(f".{group}.", "", raw) even when
379+
group was empty, which reduced the pattern to `..` and deleted every
380+
pair of characters, leaving a stub like `1` that name_process couldn't
381+
split into en/zh/jp.
382+
"""
383+
384+
def test_mixed_cjk_and_en_without_group(self):
385+
content = (
386+
"冰之城墙「氷の城壁」The Ramparts of Ice S01E02 1080p 日英双语-多国字幕"
387+
)
388+
info = raw_parser(content)
389+
assert info is not None
390+
assert info.episode == 2
391+
assert info.season == 1
392+
# Before the fix all three title fields were None and title_parser
393+
# raised "Cannot extract title_raw". At least one must now be set.
394+
assert any([info.title_en, info.title_zh, info.title_jp])

backend/src/test/test_request_url.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ async def test_client_has_max_connections(self):
3333
assert pool._max_connections is not None
3434
assert pool._max_connections > 0
3535

36+
async def test_client_follows_redirects(self):
37+
"""Regression for #983: mikanime mirror returns 302 to the canonical
38+
URL but httpx refuses to follow by default, so the RSS fetch fails."""
39+
client = await get_shared_client()
40+
assert client.follow_redirects is True
41+
3642

3743
class TestResetSharedClient:
3844
async def test_reset_closes_existing_client(self):

0 commit comments

Comments
 (0)