kmoe-manga-downloader 1.2.1__py3-none-any.whl → 1.2.3b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. kmdr/__init__.py +4 -0
  2. kmdr/_version.py +34 -0
  3. kmdr/core/__init__.py +6 -4
  4. kmdr/core/bases.py +24 -19
  5. kmdr/core/console.py +67 -0
  6. kmdr/core/constants.py +17 -22
  7. kmdr/core/context.py +18 -3
  8. kmdr/core/defaults.py +22 -7
  9. kmdr/core/error.py +23 -1
  10. kmdr/core/protocol.py +10 -0
  11. kmdr/core/session.py +111 -8
  12. kmdr/core/utils.py +51 -2
  13. kmdr/main.py +31 -10
  14. kmdr/module/authenticator/CookieAuthenticator.py +4 -8
  15. kmdr/module/authenticator/LoginAuthenticator.py +8 -21
  16. kmdr/module/authenticator/utils.py +16 -19
  17. kmdr/module/configurer/BaseUrlUpdator.py +3 -2
  18. kmdr/module/configurer/ConfigClearer.py +3 -2
  19. kmdr/module/configurer/ConfigUnsetter.py +3 -2
  20. kmdr/module/configurer/OptionLister.py +3 -2
  21. kmdr/module/configurer/OptionSetter.py +3 -2
  22. kmdr/module/configurer/option_validate.py +11 -11
  23. kmdr/module/downloader/DirectDownloader.py +10 -13
  24. kmdr/module/downloader/ReferViaDownloader.py +11 -12
  25. kmdr/module/downloader/download_utils.py +53 -7
  26. kmdr/module/downloader/misc.py +2 -0
  27. kmdr/module/lister/FollowedBookLister.py +4 -4
  28. kmdr/module/lister/utils.py +10 -8
  29. kmdr/module/picker/DefaultVolPicker.py +2 -1
  30. kmdr/module/picker/utils.py +14 -6
  31. {kmoe_manga_downloader-1.2.1.dist-info → kmoe_manga_downloader-1.2.3b0.dist-info}/METADATA +1 -1
  32. kmoe_manga_downloader-1.2.3b0.dist-info/RECORD +46 -0
  33. kmoe_manga_downloader-1.2.1.dist-info/RECORD +0 -43
  34. {kmoe_manga_downloader-1.2.1.dist-info → kmoe_manga_downloader-1.2.3b0.dist-info}/WHEEL +0 -0
  35. {kmoe_manga_downloader-1.2.1.dist-info → kmoe_manga_downloader-1.2.3b0.dist-info}/entry_points.txt +0 -0
  36. {kmoe_manga_downloader-1.2.1.dist-info → kmoe_manga_downloader-1.2.3b0.dist-info}/licenses/LICENSE +0 -0
  37. {kmoe_manga_downloader-1.2.1.dist-info → kmoe_manga_downloader-1.2.3b0.dist-info}/top_level.txt +0 -0
kmdr/main.py CHANGED
@@ -1,3 +1,5 @@
1
+ from kmdr import __version__
2
+
1
3
  from typing import Callable
2
4
  from argparse import Namespace
3
5
  import asyncio
@@ -7,29 +9,39 @@ from kmdr.module import *
7
9
 
8
10
  async def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
9
11
 
10
- if args.command == 'config':
11
- CONFIGURER.get(args).operate()
12
- return
12
+ post_init(args)
13
+ log('[Lifecycle:Start] 启动 kmdr 版本:', __version__)
14
+ debug('[bold green]以调试模式启动[/bold green]')
15
+ debug('接收到的参数:', args)
13
16
 
14
- async with KMDR_SESSION.get(args):
17
+ if args.command == 'version':
18
+ info(f"[green]{__version__}[/green]")
19
+
20
+ elif args.command == 'config':
21
+ CONFIGURER.get(args).operate()
15
22
 
16
- if args.command == 'login':
23
+ elif args.command == 'login':
24
+ async with (await SESSION_MANAGER.get(args).session()):
17
25
  await AUTHENTICATOR.get(args).authenticate()
18
26
 
19
- elif args.command == 'status':
27
+ elif args.command == 'status':
28
+ async with (await SESSION_MANAGER.get(args).session()):
20
29
  await AUTHENTICATOR.get(args).authenticate()
21
30
 
22
- elif args.command == 'download':
31
+ elif args.command == 'download':
32
+ async with (await SESSION_MANAGER.get(args).session()):
23
33
  await AUTHENTICATOR.get(args).authenticate()
24
34
 
25
35
  book, volumes = await LISTERS.get(args).list()
36
+ debug("获取到书籍《", book.name, "》及其", len(volumes), "个章节信息。")
26
37
 
27
38
  volumes = PICKERS.get(args).pick(volumes)
39
+ debug("选择了", len(volumes), "个章节进行下载:", ', '.join(volume.name for volume in volumes))
28
40
 
29
41
  await DOWNLOADER.get(args).download(book, volumes)
30
42
 
31
- else:
32
- fallback()
43
+ else:
44
+ fallback()
33
45
 
34
46
  def main_sync(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
35
47
  asyncio.run(main(args, fallback))
@@ -41,9 +53,18 @@ def entry_point():
41
53
 
42
54
  main_coro = main(args, lambda: parser.print_help())
43
55
  asyncio.run(main_coro)
56
+ except KmdrError as e:
57
+ info(f"[red]错误: {e}[/red]")
58
+ exit(1)
44
59
  except KeyboardInterrupt:
45
- print("\n操作已取消(KeyboardInterrupt)")
60
+ info("\n操作已取消(KeyboardInterrupt)", style="yellow")
46
61
  exit(130)
62
+ except Exception as e:
63
+ exception(e)
64
+ exit(1)
65
+ finally:
66
+ log('[Lifecycle:End] 运行结束,kmdr 已退出')
67
+
47
68
 
48
69
  if __name__ == '__main__':
49
70
  entry_point()
@@ -4,11 +4,11 @@ from yarl import URL
4
4
 
5
5
  from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
6
6
 
7
- from .utils import check_status, extract_base_url
7
+ from .utils import check_status
8
8
 
9
9
  @AUTHENTICATOR.register()
10
10
  class CookieAuthenticator(Authenticator):
11
- def __init__(self, proxy: Optional[str] = None, book_url: Optional[str] = None, *args, **kwargs):
11
+ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
12
12
  super().__init__(proxy, *args, **kwargs)
13
13
 
14
14
  if 'command' in kwargs and kwargs['command'] == 'status':
@@ -16,20 +16,16 @@ class CookieAuthenticator(Authenticator):
16
16
  else:
17
17
  self._show_quota = False
18
18
 
19
- # 根据用户提供的 book_url 来决定访问的镜像站
20
- self._inner_base_url = extract_base_url(book_url, default=self._base_url)
21
-
22
19
  async def _authenticate(self) -> bool:
23
20
  cookie = self._configurer.cookie
24
21
 
25
22
  if not cookie:
26
23
  raise LoginError("无法找到 Cookie,请先完成登录。", ['kmdr login -u <username>'])
27
-
28
- self._session.cookie_jar.update_cookies(cookie, response_url=URL(self.base_url))
24
+
25
+ self._session.cookie_jar.update_cookies(cookie, response_url=URL(self._base_url))
29
26
  return await check_status(
30
27
  self._session,
31
28
  self._console,
32
- base_url=self.base_url,
33
29
  show_quota=self._show_quota,
34
30
  is_vip_setter=lambda value: setattr(self._profile, 'is_vip', value),
35
31
  level_setter=lambda value: setattr(self._profile, 'user_level', value),
@@ -1,24 +1,14 @@
1
1
  from typing import Optional
2
2
  import re
3
3
  from yarl import URL
4
- from urllib.parse import urljoin
5
4
 
6
5
  from rich.prompt import Prompt
7
6
 
8
7
  from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
9
- from kmdr.core.constants import API_ROUTE
10
- from kmdr.core.error import RedirectError
8
+ from kmdr.core.constants import API_ROUTE, LoginResponse
11
9
 
12
- from .utils import check_status, extract_base_url
10
+ from .utils import check_status
13
11
 
14
- CODE_OK = 'm100'
15
-
16
- CODE_MAPPING = {
17
- 'e400': "帳號或密碼錯誤。",
18
- 'e401': "非法訪問,請使用瀏覽器正常打開本站",
19
- 'e402': "帳號已經註銷。不會解釋原因,無需提問。",
20
- 'e403': "驗證失效,請刷新頁面重新操作。",
21
- }
22
12
 
23
13
  @AUTHENTICATOR.register(
24
14
  hasvalues = {'command': 'login'}
@@ -37,31 +27,28 @@ class LoginAuthenticator(Authenticator):
37
27
  async def _authenticate(self) -> bool:
38
28
 
39
29
  async with self._session.post(
40
- url = urljoin(self._base_url, API_ROUTE.LOGIN_DO),
30
+ url = API_ROUTE.LOGIN_DO,
41
31
  data = {
42
32
  'email': self._username,
43
33
  'passwd': self._password,
44
34
  'keepalive': 'on'
45
35
  },
46
- allow_redirects = False
47
36
  ) as response:
48
37
 
49
38
  response.raise_for_status()
50
39
 
51
- if response.status in (301, 302, 307, 308) and 'Location' in response.headers:
52
- new_location = response.headers['Location']
53
- raise RedirectError("检测到重定向", new_base_url=extract_base_url(new_location) or self._base_url)
54
-
55
40
  match = re.search(r'"\w+"', await response.text())
56
41
 
57
42
  if not match:
58
43
  raise LoginError("无法解析登录响应。")
59
44
 
60
45
  code = match.group(0).split('"')[1]
61
- if code != CODE_OK:
62
- raise LoginError(f"认证失败,错误代码:{code} " + CODE_MAPPING.get(code, "未知错误。"))
63
46
 
64
- if await check_status(self._session, self._console, base_url=self.base_url, show_quota=self._show_quota):
47
+ login_response = LoginResponse.from_code(code)
48
+ if not LoginResponse.ok(login_response):
49
+ raise LoginError(f"认证失败,错误代码:{login_response.name} {login_response.value}" )
50
+
51
+ if await check_status(self._session, self._console, show_quota=self._show_quota):
65
52
  cookie = self._session.cookie_jar.filter_cookies(URL(self._base_url))
66
53
  self._configurer.cookie = {key: morsel.value for key, morsel in cookie.items()}
67
54
 
@@ -2,11 +2,13 @@ from typing import Optional, Callable
2
2
 
3
3
  from aiohttp import ClientSession
4
4
  from rich.console import Console
5
- from urllib.parse import urljoin, urlsplit
5
+
6
+ from yarl import URL
6
7
 
7
8
  from kmdr.core.error import LoginError
8
9
  from kmdr.core.utils import async_retry
9
- from kmdr.core.constants import API_ROUTE, BASE_URL
10
+ from kmdr.core.constants import API_ROUTE
11
+ from kmdr.core.console import *
10
12
 
11
13
  NICKNAME_ID = 'div_nickname_display'
12
14
 
@@ -18,20 +20,19 @@ LV1_ID = 'div_user_lv1'
18
20
  async def check_status(
19
21
  session: ClientSession,
20
22
  console: Console,
21
- base_url: str = BASE_URL.KXO,
22
23
  show_quota: bool = False,
23
24
  is_vip_setter: Optional[Callable[[int], None]] = None,
24
25
  level_setter: Optional[Callable[[int], None]] = None,
25
26
  ) -> bool:
26
- async with session.get(url = urljoin(base_url, API_ROUTE.PROFILE)) as response:
27
+ async with session.get(url = API_ROUTE.PROFILE) as response:
27
28
  try:
28
29
  response.raise_for_status()
29
30
  except Exception as e:
30
- console.print(f"Error: {type(e).__name__}: {e}")
31
+ info(f"Error: {type(e).__name__}: {e}")
31
32
  return False
32
33
 
33
34
  if response.history and any(resp.status in (301, 302, 307) for resp in response.history) \
34
- and str(response.url) == urljoin(base_url, API_ROUTE.LOGIN):
35
+ and URL(response.url).path == API_ROUTE.LOGIN:
35
36
  raise LoginError("凭证已失效,请重新登录。", ['kmdr config -c cookie', 'kmdr login -u <username>'])
36
37
 
37
38
  if not is_vip_setter and not level_setter and not show_quota:
@@ -50,6 +51,8 @@ async def check_status(
50
51
  is_vip = int(var_define.get('is_vip', '0'))
51
52
  user_level = int(var_define.get('user_level', '0'))
52
53
 
54
+ debug("解析到用户状态: is_vip=", is_vip, ", user_level=", user_level)
55
+
53
56
  if is_vip_setter:
54
57
  is_vip_setter(is_vip)
55
58
  if level_setter:
@@ -58,10 +61,14 @@ async def check_status(
58
61
  if not show_quota:
59
62
  return True
60
63
 
61
- nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0]
62
- quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip()
64
+ nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0].replace('\xa0', '')
65
+ quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip().replace('\xa0', '')
66
+
67
+ if console.is_interactive:
68
+ info(f"\n当前登录为 [bold cyan]{nickname}[/bold cyan]\n\n{quota}")
69
+ else:
70
+ info(f"当前登录为 {nickname}")
63
71
 
64
- console.print(f"\n当前登录为 [bold cyan]{nickname}[/bold cyan]\n\n{quota}")
65
72
  return True
66
73
 
67
74
  def extract_var_define(script_text) -> dict[str, str]:
@@ -81,13 +88,3 @@ def __resolve_quota_id(is_vip: Optional[int] = None, user_level: Optional[int] =
81
88
  return LV1_ID
82
89
 
83
90
  return NOR_ID
84
-
85
- def extract_base_url(url: Optional[str], default: Optional[str] = None) -> Optional[str]:
86
- if not url:
87
- return default
88
-
89
- parsed = urlsplit(url)
90
- if parsed.scheme and parsed.netloc:
91
- return f"{parsed.scheme}://{parsed.netloc}"
92
-
93
- return default
@@ -1,4 +1,5 @@
1
1
  from kmdr.core import Configurer, CONFIGURER
2
+ from kmdr.core.console import info
2
3
 
3
4
  @CONFIGURER.register()
4
5
  class BaseUrlUpdator(Configurer):
@@ -10,7 +11,7 @@ class BaseUrlUpdator(Configurer):
10
11
  try:
11
12
  self._configurer.set_base_url(self._base_url)
12
13
  except KeyError as e:
13
- self._console.print(e.args[0])
14
+ info(f"[red]{e.args[0]}[/red]")
14
15
  exit(1)
15
16
 
16
- self._console.print(f"已设置基础 URL: {self._base_url}")
17
+ info(f"已设置基础 URL: {self._base_url}")
@@ -1,4 +1,5 @@
1
1
  from kmdr.core import Configurer, CONFIGURER
2
+ from kmdr.core.console import info
2
3
 
3
4
  @CONFIGURER.register()
4
5
  class ConfigClearer(Configurer):
@@ -10,7 +11,7 @@ class ConfigClearer(Configurer):
10
11
  try:
11
12
  self._configurer.clear(self._clear)
12
13
  except KeyError as e:
13
- self._console.print(e.args[0])
14
+ info(f"[red]{e.args[0]}[/red]")
14
15
  exit(1)
15
16
 
16
- self._console.print(f"Cleared configuration: {self._clear}")
17
+ info(f"Cleared configuration: {self._clear}")
@@ -1,4 +1,5 @@
1
1
  from kmdr.core import Configurer, CONFIGURER
2
+ from kmdr.core.console import info
2
3
 
3
4
  from .option_validate import check_key
4
5
 
@@ -10,9 +11,9 @@ class ConfigUnsetter(Configurer):
10
11
 
11
12
  def operate(self) -> None:
12
13
  if not self._unset:
13
- self._console.print("[yellow]请提供要取消设置的配置项。[/yellow]")
14
+ info("[yellow]请提供要取消设置的配置项。[/yellow]")
14
15
  return
15
16
 
16
17
  check_key(self._unset)
17
18
  self._configurer.unset_option(self._unset)
18
- self._console.print(f"[green]取消配置项: {self._unset}[/green]")
19
+ info(f"[green]取消配置项: {self._unset}[/green]")
@@ -2,6 +2,7 @@ from rich.table import Table
2
2
  from rich.pretty import Pretty
3
3
 
4
4
  from kmdr.core import CONFIGURER, Configurer
5
+ from kmdr.core.console import info
5
6
 
6
7
  @CONFIGURER.register(
7
8
  hasvalues={
@@ -14,7 +15,7 @@ class OptionLister(Configurer):
14
15
 
15
16
  def operate(self) -> None:
16
17
  if self._configurer.option is None and self._configurer.base_url is None:
17
- self._console.print("[blue]当前没有任何配置项。[/blue]")
18
+ info("[blue]当前没有任何配置项。[/blue]")
18
19
  return
19
20
 
20
21
  table = Table(title="[green]当前 Kmdr 配置项[/green]", show_header=False, header_style="blue")
@@ -35,4 +36,4 @@ class OptionLister(Configurer):
35
36
  if self._configurer.base_url is not None:
36
37
  table.add_row('应用配置', '镜像地址', self._configurer.base_url or '未设置')
37
38
 
38
- self._console.print(table)
39
+ info(table)
@@ -1,4 +1,5 @@
1
1
  from kmdr.core import Configurer, CONFIGURER
2
+ from kmdr.core.console import info
2
3
 
3
4
  from .option_validate import validate
4
5
 
@@ -11,7 +12,7 @@ class OptionSetter(Configurer):
11
12
  def operate(self) -> None:
12
13
  for option in self._set:
13
14
  if '=' not in option:
14
- self._console.print(f"[red]无效的选项格式: `{option}`。[/red] 应为 key=value 格式。")
15
+ info(f"[red]无效的选项格式: `{option}`。[/red] 应为 key=value 格式。")
15
16
  continue
16
17
 
17
18
  key, value = option.split('=', 1)
@@ -23,7 +24,7 @@ class OptionSetter(Configurer):
23
24
  continue
24
25
 
25
26
  self._configurer.set_option(key, validated_value)
26
- self._console.print(f"[green]已设置配置: {key} = {validated_value}[/green]")
27
+ info(f"[green]已设置配置: {key} = {validated_value}[/green]")
27
28
 
28
29
 
29
30
 
@@ -2,7 +2,7 @@ from typing import Optional
2
2
  from functools import wraps
3
3
  import os
4
4
 
5
- from kmdr.core.defaults import console_print as print
5
+ from kmdr.core.console import info
6
6
 
7
7
  __OPTIONS_VALIDATOR = {}
8
8
 
@@ -17,7 +17,7 @@ def validate(key: str, value: str) -> Optional[object]:
17
17
  if key in __OPTIONS_VALIDATOR:
18
18
  return __OPTIONS_VALIDATOR[key](value)
19
19
  else:
20
- print(f"[red]不支持的配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
20
+ info(f"[red]不支持的配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
21
21
  return None
22
22
 
23
23
  def check_key(key: str, exit_if_invalid: bool = True) -> None:
@@ -29,7 +29,7 @@ def check_key(key: str, exit_if_invalid: bool = True) -> None:
29
29
  :param exit_if_invalid: 如果键名无效,是否退出程序
30
30
  """
31
31
  if key not in __OPTIONS_VALIDATOR:
32
- print(f"[red]未知配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
32
+ info(f"[red]未知配置项: {key}。可用配置项:{', '.join(__OPTIONS_VALIDATOR.keys())}[/red]")
33
33
  if exit_if_invalid:
34
34
  exit(1)
35
35
 
@@ -65,24 +65,24 @@ def validate_num_workers(value: str) -> Optional[int]:
65
65
  raise ValueError("必须是正值。")
66
66
  return num_workers
67
67
  except ValueError as e:
68
- print(f"[red]无效的 num_workers 值: {value}。{str(e)}[/red]")
68
+ info(f"[red]无效的 num_workers 值: {value}。{str(e)}[/red]")
69
69
  return None
70
70
 
71
71
  @register_validator('dest')
72
72
  def validate_dest(value: str) -> Optional[str]:
73
73
  if not value:
74
- print("[red]目标目录不能为空。[/red]")
74
+ info("[red]目标目录不能为空。[/red]")
75
75
  return None
76
76
  if not os.path.exists(value) or not os.path.isdir(value):
77
- print(f"[red]目标目录不存在或不是目录: {value}[/red]")
77
+ info(f"[red]目标目录不存在或不是目录: {value}[/red]")
78
78
  return None
79
79
 
80
80
  if not os.access(value, os.W_OK):
81
- print(f"[red]目标目录不可写: {value}[/red]")
81
+ info(f"[red]目标目录不可写: {value}[/red]")
82
82
  return None
83
83
 
84
84
  if not os.path.isabs(value):
85
- print(f"[yellow]目标目录最好是绝对路径: {value}[/yellow]")
85
+ info(f"[yellow]目标目录最好是绝对路径: {value}[/yellow]")
86
86
 
87
87
  return value
88
88
 
@@ -94,19 +94,19 @@ def validate_retry(value: str) -> Optional[int]:
94
94
  raise ValueError("必须是正值。")
95
95
  return retry
96
96
  except ValueError as e:
97
- print(f"[red]无效的 retry 值: {value}。{str(e)}[/red]")
97
+ info(f"[red]无效的 retry 值: {value}。{str(e)}[/red]")
98
98
  return None
99
99
 
100
100
  @register_validator('callback')
101
101
  def validate_callback(value: str) -> Optional[str]:
102
102
  if not value:
103
- print("[red]回调不能为空。[/red]")
103
+ info("[red]回调不能为空。[/red]")
104
104
  return None
105
105
  return value
106
106
 
107
107
  @register_validator('proxy')
108
108
  def validate_proxy(value: str) -> Optional[str]:
109
109
  if not value:
110
- print("[red]代理不能为空。[/red]")
110
+ info("[red]代理不能为空。[/red]")
111
111
  return None
112
112
  return value
@@ -1,13 +1,13 @@
1
- from urllib.parse import urljoin
1
+ from functools import partial
2
2
 
3
3
  from kmdr.core import Downloader, BookInfo, VolInfo, DOWNLOADER
4
4
  from kmdr.core.constants import API_ROUTE
5
5
 
6
- from .download_utils import safe_filename, download_file_multipart
6
+ from .download_utils import download_file_multipart, readable_safe_filename
7
7
 
8
8
  @DOWNLOADER.register(
9
9
  hasvalues={
10
- 'method': 1
10
+ 'method': 2
11
11
  }
12
12
  )
13
13
  class DirectDownloader(Downloader):
@@ -15,26 +15,23 @@ class DirectDownloader(Downloader):
15
15
  super().__init__(dest, callback, retry, num_workers, proxy, *args, **kwargs)
16
16
 
17
17
  async def _download(self, book: BookInfo, volume: VolInfo):
18
- sub_dir = safe_filename(book.name)
18
+ sub_dir = readable_safe_filename(book.name)
19
19
  download_path = f'{self._dest}/{sub_dir}'
20
20
 
21
21
  await download_file_multipart(
22
22
  self._session,
23
23
  self._semaphore,
24
24
  self._progress,
25
- lambda: self.construct_download_url(book, volume),
25
+ partial(self.construct_download_url, book, volume),
26
26
  download_path,
27
- safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
27
+ readable_safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
28
28
  self._retry,
29
29
  callback=lambda: self._callback(book, volume) if self._callback else None
30
30
  )
31
31
 
32
32
  def construct_download_url(self, book: BookInfo, volume: VolInfo) -> str:
33
- return urljoin(
34
- self._base_url,
35
- API_ROUTE.DOWNLOAD.format(
36
- book_id=book.id,
37
- volume_id=volume.id,
38
- is_vip=self._profile.is_vip
39
- )
33
+ return API_ROUTE.DOWNLOAD.format(
34
+ book_id=book.id,
35
+ volume_id=volume.id,
36
+ is_vip=self._profile.is_vip
40
37
  )
@@ -1,13 +1,14 @@
1
1
  from functools import partial
2
- from urllib.parse import urljoin
3
2
 
4
3
  import json
5
4
  from async_lru import alru_cache
6
5
 
7
6
  from kmdr.core import Downloader, VolInfo, DOWNLOADER, BookInfo
8
7
  from kmdr.core.constants import API_ROUTE
8
+ from kmdr.core.error import ResponseError
9
+ from kmdr.core.console import debug
9
10
 
10
- from .download_utils import safe_filename, download_file_multipart
11
+ from .download_utils import download_file_multipart, readable_safe_filename
11
12
 
12
13
 
13
14
  @DOWNLOADER.register(order=10)
@@ -17,7 +18,7 @@ class ReferViaDownloader(Downloader):
17
18
 
18
19
 
19
20
  async def _download(self, book: BookInfo, volume: VolInfo):
20
- sub_dir = safe_filename(book.name)
21
+ sub_dir = readable_safe_filename(book.name)
21
22
  download_path = f'{self._dest}/{sub_dir}'
22
23
 
23
24
  await download_file_multipart(
@@ -26,7 +27,7 @@ class ReferViaDownloader(Downloader):
26
27
  self._progress,
27
28
  partial(self.fetch_download_url, book.id, volume.id),
28
29
  download_path,
29
- safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
30
+ readable_safe_filename(f'[Kmoe][{book.name}][{volume.name}].epub'),
30
31
  self._retry,
31
32
  headers={
32
33
  "X-Km-From": "kb_http_down"
@@ -37,20 +38,18 @@ class ReferViaDownloader(Downloader):
37
38
  @alru_cache(maxsize=128)
38
39
  async def fetch_download_url(self, book_id: str, volume_id: str) -> str:
39
40
 
40
- url = urljoin(
41
- self._base_url,
41
+ async with self._session.get(
42
42
  API_ROUTE.GETDOWNURL.format(
43
43
  book_id=book_id,
44
44
  volume_id=volume_id,
45
45
  is_vip=self._profile.is_vip
46
46
  )
47
- )
48
-
49
- async with self._session.get(url) as response:
47
+ ) as response:
50
48
  response.raise_for_status()
51
49
  data = await response.text()
52
50
  data = json.loads(data)
53
- if data.get('code') != 200:
54
- raise Exception(f"Failed to fetch download URL: {data.get('msg', 'Unknown error')}")
55
-
51
+ debug("获取下载链接响应数据:", data)
52
+ if (code := data.get('code')) != 200:
53
+ raise ResponseError(f"Failed to fetch download URL: {data.get('msg', 'Unknown error')}", code)
54
+
56
55
  return data['url']