kmoe-manga-downloader 1.1.2__py3-none-any.whl → 1.2.0__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 (31) hide show
  1. kmdr/core/__init__.py +5 -3
  2. kmdr/core/bases.py +49 -87
  3. kmdr/core/context.py +28 -0
  4. kmdr/core/defaults.py +64 -28
  5. kmdr/core/error.py +1 -1
  6. kmdr/core/session.py +16 -0
  7. kmdr/core/utils.py +33 -40
  8. kmdr/main.py +24 -15
  9. kmdr/module/authenticator/CookieAuthenticator.py +8 -4
  10. kmdr/module/authenticator/LoginAuthenticator.py +25 -21
  11. kmdr/module/authenticator/utils.py +47 -43
  12. kmdr/module/configurer/ConfigClearer.py +7 -2
  13. kmdr/module/configurer/ConfigUnsetter.py +2 -2
  14. kmdr/module/configurer/OptionLister.py +17 -3
  15. kmdr/module/configurer/OptionSetter.py +2 -2
  16. kmdr/module/configurer/option_validate.py +14 -12
  17. kmdr/module/downloader/DirectDownloader.py +7 -5
  18. kmdr/module/downloader/ReferViaDownloader.py +27 -24
  19. kmdr/module/downloader/utils.py +249 -91
  20. kmdr/module/lister/BookUrlLister.py +4 -3
  21. kmdr/module/lister/FollowedBookLister.py +59 -22
  22. kmdr/module/lister/utils.py +39 -28
  23. kmdr/module/picker/ArgsFilterPicker.py +1 -1
  24. kmdr/module/picker/DefaultVolPicker.py +34 -5
  25. {kmoe_manga_downloader-1.1.2.dist-info → kmoe_manga_downloader-1.2.0.dist-info}/METADATA +13 -11
  26. kmoe_manga_downloader-1.2.0.dist-info/RECORD +35 -0
  27. kmoe_manga_downloader-1.1.2.dist-info/RECORD +0 -33
  28. {kmoe_manga_downloader-1.1.2.dist-info → kmoe_manga_downloader-1.2.0.dist-info}/WHEEL +0 -0
  29. {kmoe_manga_downloader-1.1.2.dist-info → kmoe_manga_downloader-1.2.0.dist-info}/entry_points.txt +0 -0
  30. {kmoe_manga_downloader-1.1.2.dist-info → kmoe_manga_downloader-1.2.0.dist-info}/licenses/LICENSE +0 -0
  31. {kmoe_manga_downloader-1.1.2.dist-info → kmoe_manga_downloader-1.2.0.dist-info}/top_level.txt +0 -0
kmdr/core/__init__.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from .bases import Authenticator, Lister, Picker, Downloader, Configurer
2
2
  from .structure import VolInfo, BookInfo, VolumeType
3
- from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER
3
+ from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER, KMDR_SESSION
4
4
 
5
- from .defaults import argument_parser
5
+ from .defaults import argument_parser, session_var
6
6
 
7
- from .error import KmdrError, LoginError
7
+ from .error import KmdrError, LoginError
8
+
9
+ from .session import KmdrSession
kmdr/core/bases.py CHANGED
@@ -1,133 +1,95 @@
1
- import os
2
-
3
1
  from typing import Callable, Optional
2
+ from abc import abstractmethod
3
+
4
+ import asyncio
5
+ from aiohttp import ClientSession
4
6
 
5
7
  from .error import LoginError
6
8
  from .registry import Registry
7
9
  from .structure import VolInfo, BookInfo
8
- from .utils import get_singleton_session, construct_callback
9
- from .defaults import Configurer as InnerConfigurer, UserProfile
10
-
11
- class SessionContext:
10
+ from .utils import construct_callback
12
11
 
13
- def __init__(self, *args, **kwargs):
14
- super().__init__()
15
- self._session = get_singleton_session()
12
+ from .context import TerminalContext, SessionContext, UserProfileContext, ConfigContext
16
13
 
17
- class UserProfileContext:
18
-
19
- def __init__(self, *args, **kwargs):
20
- super().__init__()
21
- self._profile = UserProfile()
22
-
23
- class ConfigContext:
24
-
25
- def __init__(self, *args, **kwargs):
26
- super().__init__()
27
- self._configurer = InnerConfigurer()
28
-
29
- class Configurer(ConfigContext):
14
+ class Configurer(ConfigContext, TerminalContext):
30
15
 
31
16
  def __init__(self, *args, **kwargs):
32
17
  super().__init__(*args, **kwargs)
33
18
 
19
+ @abstractmethod
34
20
  def operate(self) -> None: ...
35
21
 
36
- class Authenticator(SessionContext, ConfigContext, UserProfileContext):
22
+ class Authenticator(SessionContext, ConfigContext, UserProfileContext, TerminalContext):
37
23
 
38
- def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
24
+ def __init__(self, *args, **kwargs):
39
25
  super().__init__(*args, **kwargs)
40
26
 
41
- if proxy:
42
- self._session.proxies.update({
43
- 'https': proxy,
44
- 'http': proxy,
45
- })
46
-
47
27
  # 在使用代理登录时,可能会出现问题,但是现在还不清楚是不是代理的问题。
48
28
  # 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
49
29
  # 所以暂时保留代理登录的功能,如果后续确认是代理的问题,可以考虑启用 @no_proxy 装饰器。
50
30
  # @no_proxy
51
- def authenticate(self) -> None:
52
- try:
53
- assert self._authenticate()
54
- except LoginError as e:
55
- print("Authentication failed. Please check your login credentials or session cookies.")
56
- print(f"Details: {e}")
57
- exit(1)
31
+ async def authenticate(self) -> None:
32
+ with self._console.status("认证中..."):
33
+ try:
34
+ assert await self._authenticate()
35
+ except LoginError as e:
36
+ self._console.print("[red]认证失败。请检查您的登录凭据或会话 cookie。[/red]")
37
+ self._console.print(f"[yellow]详细信息:{e}[/yellow]")
38
+ exit(1)
58
39
 
59
- def _authenticate(self) -> bool: ...
40
+ @abstractmethod
41
+ async def _authenticate(self) -> bool: ...
60
42
 
61
- class Lister(SessionContext):
43
+ class Lister(SessionContext, TerminalContext):
62
44
 
63
45
  def __init__(self, *args, **kwargs):
64
46
  super().__init__(*args, **kwargs)
65
47
 
66
- def list(self) -> tuple[BookInfo, list[VolInfo]]: ...
48
+ @abstractmethod
49
+ async def list(self) -> tuple[BookInfo, list[VolInfo]]: ...
67
50
 
68
- class Picker(SessionContext):
51
+ class Picker(TerminalContext):
69
52
 
70
53
  def __init__(self, *args, **kwargs):
71
54
  super().__init__(*args, **kwargs)
72
55
 
56
+ @abstractmethod
73
57
  def pick(self, volumes: list[VolInfo]) -> list[VolInfo]: ...
74
58
 
75
- class Downloader(SessionContext, UserProfileContext):
59
+ class Downloader(SessionContext, UserProfileContext, TerminalContext):
76
60
 
77
- def __init__(self,
78
- dest: str = '.',
79
- callback: Optional[str] = None,
80
- retry: int = 3,
81
- num_workers: int = 1,
82
- proxy: Optional[str] = None,
83
- *args, **kwargs
61
+ def __init__(self,
62
+ dest: str = '.',
63
+ callback: Optional[str] = None,
64
+ retry: int = 3,
65
+ num_workers: int = 8,
66
+ *args, **kwargs
84
67
  ):
85
68
  super().__init__(*args, **kwargs)
69
+
86
70
  self._dest: str = dest
87
- self._callback: Optional[Callable[[BookInfo, VolInfo], int]] = construct_callback(callback)
71
+ self._callback: Optional[Callable] = construct_callback(callback)
88
72
  self._retry: int = retry
89
- self._num_workers: int = num_workers
90
-
91
- if proxy:
92
- self._session.proxies.update({
93
- 'https': proxy,
94
- 'http': proxy,
95
- })
96
-
97
- def download(self, book: BookInfo, volumes: list[VolInfo]):
98
- if volumes is None or not volumes:
99
- raise ValueError("No volumes to download")
73
+ self._semaphore = asyncio.Semaphore(num_workers)
100
74
 
101
- if self._num_workers <= 1:
102
- for volume in volumes:
103
- self._download(book, volume, self._retry)
104
- else:
105
- self._download_with_multiple_workers(book, volumes, self._retry)
106
-
107
- def _download(self, book: BookInfo, volume: VolInfo, retry: int): ...
108
-
109
- def _download_with_multiple_workers(self, book: BookInfo, volumes: list[VolInfo], retry: int):
110
- from concurrent.futures import ThreadPoolExecutor, wait, FIRST_EXCEPTION
75
+ async def download(self, book: BookInfo, volumes: list[VolInfo]):
76
+ if not volumes:
77
+ self._console.print("No volumes to download.")
78
+ exit(0)
111
79
 
112
80
  try:
113
- max_workers = min(self._num_workers, len(volumes))
114
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
115
- futures = [
116
- executor.submit(self._download, book, volume, retry)
117
- for volume in volumes
118
- ]
119
- wait(futures, return_when=FIRST_EXCEPTION)
120
- for future in futures:
121
- future.result()
81
+ with self._progress:
82
+ tasks = [self._download(book, volume) for volume in volumes]
83
+ await asyncio.gather(*tasks, return_exceptions=True)
84
+
122
85
  except KeyboardInterrupt:
123
- print("\n操作已取消(KeyboardInterrupt)")
124
- try:
125
- executor.shutdown(wait=False, cancel_futures=True)
126
- except NameError:
127
- pass
128
- finally:
129
- exit(130)
86
+ self._console.print("\n操作已取消(KeyboardInterrupt)")
87
+ exit(130)
88
+
89
+ @abstractmethod
90
+ async def _download(self, book: BookInfo, volume: VolInfo): ...
130
91
 
92
+ KMDR_SESSION = Registry[ClientSession]('KmdrSession', True)
131
93
  AUTHENTICATOR = Registry[Authenticator]('Authenticator')
132
94
  LISTERS = Registry[Lister]('Lister')
133
95
  PICKERS = Registry[Picker]('Picker')
kmdr/core/context.py ADDED
@@ -0,0 +1,28 @@
1
+ from aiohttp import ClientSession
2
+
3
+ from .defaults import Configurer as InnerConfigurer, UserProfile, session_var, progress, console
4
+
5
+ class TerminalContext:
6
+
7
+ def __init__(self, *args, **kwargs):
8
+ super().__init__()
9
+ self._progress = progress
10
+ self._console = console
11
+
12
+ class SessionContext:
13
+
14
+ def __init__(self, *args, **kwargs):
15
+ super().__init__()
16
+ self._session: ClientSession = session_var.get()
17
+
18
+ class UserProfileContext:
19
+
20
+ def __init__(self, *args, **kwargs):
21
+ super().__init__()
22
+ self._profile = UserProfile()
23
+
24
+ class ConfigContext:
25
+
26
+ def __init__(self, *args, **kwargs):
27
+ super().__init__()
28
+ self._configurer = InnerConfigurer()
kmdr/core/defaults.py CHANGED
@@ -2,10 +2,46 @@ import os
2
2
  import json
3
3
  from typing import Optional
4
4
  import argparse
5
+ from contextvars import ContextVar
6
+ from rich.console import Console
7
+ from rich.progress import (
8
+ Progress,
9
+ BarColumn,
10
+ DownloadColumn,
11
+ TextColumn,
12
+ TransferSpeedColumn,
13
+ TimeRemainingColumn,
14
+ )
5
15
 
6
16
  from .utils import singleton
7
17
  from .structure import Config
8
18
 
19
+ HEADERS = {
20
+ 'User-Agent': 'kmdr/1.0 (https://github.com/chrisis58/kmoe-manga-downloader)'
21
+ }
22
+
23
+ console = Console()
24
+
25
+ def console_print(*args, **kwargs):
26
+ console.print(*args, **kwargs)
27
+
28
+ progress = Progress(
29
+ TextColumn("[blue]{task.fields[filename]}", justify="left"),
30
+ TextColumn("{task.fields[status]}", justify="right"),
31
+ TextColumn("{task.percentage:>3.1f}%"),
32
+ BarColumn(bar_width=None),
33
+ "[progress.percentage]",
34
+ DownloadColumn(),
35
+ "[",
36
+ TransferSpeedColumn(),
37
+ ",",
38
+ TimeRemainingColumn(),
39
+ "]",
40
+ console=console,
41
+ )
42
+
43
+ session_var = ContextVar('session')
44
+
9
45
  parser: Optional[argparse.ArgumentParser] = None
10
46
  args: Optional[argparse.Namespace] = None
11
47
 
@@ -14,33 +50,33 @@ def argument_parser():
14
50
  if parser is not None:
15
51
  return parser
16
52
 
17
- parser = argparse.ArgumentParser(description='Kox Downloader')
18
- subparsers = parser.add_subparsers(title='subcommands', dest='command')
19
-
20
- download_parser = subparsers.add_parser('download', help='Download books')
21
- download_parser.add_argument('-d', '--dest', type=str, help='Download destination, default to current directory', required=False)
22
- download_parser.add_argument('-l', '--book-url', type=str, help='Book page\'s url', required=False)
23
- download_parser.add_argument('-v', '--volume', type=str, help='Volume(s), split using commas, `all` for all', required=False)
24
- download_parser.add_argument('-t', '--vol-type', type=str, help='Volume type, `vol` for volume, `extra` for extras, `seri` for serialized', required=False, choices=['vol', 'extra', 'seri', 'all'], default='vol')
25
- download_parser.add_argument('--max-size', type=float, help='Max size of volume in MB', required=False)
26
- download_parser.add_argument('--limit', type=int, help='Limit number of volumes to download', required=False)
27
- download_parser.add_argument('--num-workers', type=int, help='Number of workers to use for downloading', required=False)
28
- download_parser.add_argument('-p', '--proxy', type=str, help='Proxy server', required=False)
29
- download_parser.add_argument('-r', '--retry', type=int, help='Retry times', required=False)
30
- download_parser.add_argument('-c', '--callback', type=str, help='Callback script, use as `echo {v.name} downloaded!`', required=False)
31
-
32
- login_parser = subparsers.add_parser('login', help='Login to kox.moe')
33
- login_parser.add_argument('-u', '--username', type=str, help='Your username', required=True)
34
- login_parser.add_argument('-p', '--password', type=str, help='Your password', required=False)
35
-
36
- status_parser = subparsers.add_parser('status', help='Show status of account and script')
37
- status_parser.add_argument('-p', '--proxy', type=str, help='Proxy server', required=False)
38
-
39
- config_parser = subparsers.add_parser('config', help='Configure the downloader')
40
- config_parser.add_argument('-l', '--list-option', action='store_true', help='List all configurations')
41
- config_parser.add_argument('-s', '--set', nargs='+', type=str, help='Configuration options to set, e.g. num_workers=3 dest=.')
42
- config_parser.add_argument('-c', '--clear', type=str, help='Clear configurations, `all`, `cookie`, `option` are available')
43
- config_parser.add_argument('-d', '--delete', '--unset', dest='unset', type=str, help='Delete a specific configuration option')
53
+ parser = argparse.ArgumentParser(description='Kmoe 漫画下载器')
54
+ subparsers = parser.add_subparsers(title='可用的子命令', dest='command')
55
+
56
+ download_parser = subparsers.add_parser('download', help='下载指定的漫画')
57
+ download_parser.add_argument('-d', '--dest', type=str, help='指定下载文件的保存路径,默认为当前目录', required=False)
58
+ download_parser.add_argument('-l', '--book-url', type=str, help='漫画详情页面的 URL', required=False)
59
+ download_parser.add_argument('-v', '--volume', type=str, help='指定下载的卷,多个用逗号分隔,例如 `1,2,3` `1-5,8`,`all` 表示全部', required=False)
60
+ download_parser.add_argument('-t', '--vol-type', type=str, help='指定下载的卷类型,`vol` 为单行本, `extra` 为番外, `seri` 为连载', required=False, choices=['vol', 'extra', 'seri', 'all'], default='vol')
61
+ download_parser.add_argument('--max-size', type=float, help='限制下载卷的最大体积 (单位: MB)', required=False)
62
+ download_parser.add_argument('--limit', type=int, help='限制下载卷的总数量', required=False)
63
+ download_parser.add_argument('--num-workers', type=int, help='下载时使用的并发任务数', required=False)
64
+ download_parser.add_argument('-p', '--proxy', type=str, help='设置下载使用的代理服务器', required=False)
65
+ download_parser.add_argument('-r', '--retry', type=int, help='网络请求失败时的重试次数', required=False)
66
+ download_parser.add_argument('-c', '--callback', type=str, help='每个卷下载完成后执行的回调脚本,例如: `echo {v.name} downloaded!`', required=False)
67
+
68
+ login_parser = subparsers.add_parser('login', help='登录到 Kmoe')
69
+ login_parser.add_argument('-u', '--username', type=str, help='用户名', required=True)
70
+ login_parser.add_argument('-p', '--password', type=str, help='密码 (如果留空,应用将提示您输入)', required=False)
71
+
72
+ status_parser = subparsers.add_parser('status', help='显示账户信息以及配额')
73
+ status_parser.add_argument('-p', '--proxy', type=str, help='代理服务器', required=False)
74
+
75
+ config_parser = subparsers.add_parser('config', help='配置下载器')
76
+ config_parser.add_argument('-l', '--list-option', action='store_true', help='列出所有配置')
77
+ config_parser.add_argument('-s', '--set', nargs='+', type=str, help='设置一个或多个配置项,格式为 `key=value`,例如: `num_workers=8`')
78
+ config_parser.add_argument('-c', '--clear', type=str, help='清除指定配置,可选值为 `all`, `cookie`, `option`')
79
+ config_parser.add_argument('-d', '--delete', '--unset', dest='unset', type=str, help='删除特定的配置选项')
44
80
 
45
81
  return parser
46
82
 
@@ -144,7 +180,7 @@ class Configurer:
144
180
  elif key == 'option':
145
181
  self._config.option = None
146
182
  else:
147
- raise ValueError(f"Unsupported clear option: {key}")
183
+ raise KeyError(f"[red]对应配置不存在: {key}。可用配置项:all, cookie, option[/red]")
148
184
 
149
185
  self.update()
150
186
 
kmdr/core/error.py CHANGED
@@ -5,7 +5,7 @@ class KmdrError(RuntimeError):
5
5
  super().__init__(message, *args, **kwargs)
6
6
  self.message = message
7
7
 
8
- self._solution = "" if solution is None else "\nSuggested Solution: \n" + "\n".join(f">>> {sol}" for sol in solution)
8
+ self._solution = "" if solution is None else "\n[bold cyan]推荐解决方法:[/bold cyan] \n" + "\n".join(f"[cyan]>>> {sol}[/cyan]" for sol in solution)
9
9
 
10
10
  class LoginError(KmdrError):
11
11
  def __init__(self, message, solution: Optional[list[str]] = None):
kmdr/core/session.py ADDED
@@ -0,0 +1,16 @@
1
+ from typing import Optional
2
+
3
+ from aiohttp import ClientSession
4
+
5
+ from .bases import KMDR_SESSION
6
+ from .defaults import session_var, HEADERS
7
+
8
+ @KMDR_SESSION.register()
9
+ class KmdrSession(ClientSession):
10
+ """
11
+ Kmdr 的 HTTP 会话管理类,支持从参数中初始化。简化 ClientSession 的使用。
12
+ """
13
+
14
+ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
15
+ ClientSession.__init__(self, proxy=proxy, trust_env=True, headers=HEADERS)
16
+ session_var.set(self)
kmdr/core/utils.py CHANGED
@@ -1,37 +1,14 @@
1
1
  import functools
2
2
  from typing import Optional, Callable
3
+ import asyncio
3
4
 
4
- from requests import Session
5
- import threading
5
+ import aiohttp
6
+
7
+ from deprecation import deprecated
6
8
  import subprocess
7
9
 
8
10
  from .structure import BookInfo, VolInfo
9
11
 
10
- _session_instance: Optional[Session] = None
11
-
12
- _session_lock = threading.Lock()
13
-
14
- HEADERS = {
15
- 'User-Agent': 'kmdr/1.0 (https://github.com/chrisis58/kmoe-manga-downloader)'
16
- }
17
-
18
- def get_singleton_session() -> Session:
19
- global _session_instance
20
-
21
- if _session_instance is None:
22
- with _session_lock:
23
- if _session_instance is None:
24
- _session_instance = Session()
25
- _session_instance.headers.update(HEADERS)
26
-
27
- return _session_instance
28
-
29
- def clear_session_context():
30
- session = get_singleton_session()
31
- session.proxies.clear()
32
- session.headers.clear()
33
- session.cookies.clear()
34
- session.headers.update(HEADERS)
35
12
 
36
13
  def singleton(cls):
37
14
  """
@@ -61,17 +38,33 @@ def construct_callback(callback: Optional[str]) -> Optional[Callable]:
61
38
 
62
39
  return _callback
63
40
 
64
- def no_proxy(func):
65
- @functools.wraps(func)
66
- def wrapper(*args, **kwargs):
67
- session = get_singleton_session()
68
-
69
- cached_proxies = session.proxies.copy()
70
- session.proxies.clear()
71
-
72
- try:
73
- return func(*args, **kwargs)
74
- finally:
75
- session.proxies = cached_proxies
76
41
 
77
- return wrapper
42
+ def async_retry(
43
+ attempts: int = 3,
44
+ delay: float = 1.0,
45
+ backoff: float = 2.0,
46
+ retry_on_status: set[int] = {500, 502, 503, 504, 429, 408}
47
+ ):
48
+ def decorator(func):
49
+ @functools.wraps(func)
50
+ async def wrapper(*args, **kwargs):
51
+ current_delay = delay
52
+ for attempt in range(attempts):
53
+ try:
54
+ return await func(*args, **kwargs)
55
+ except aiohttp.ClientResponseError as e:
56
+ if e.status in retry_on_status:
57
+ if attempt == attempts - 1:
58
+ raise
59
+ else:
60
+ raise
61
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
62
+ # 对于所有其他 aiohttp 客户端异常和超时,进行重试
63
+ if attempt == attempts - 1:
64
+ raise
65
+
66
+ await asyncio.sleep(current_delay)
67
+
68
+ current_delay *= backoff
69
+ return wrapper
70
+ return decorator
kmdr/main.py CHANGED
@@ -1,37 +1,46 @@
1
1
  from typing import Callable
2
2
  from argparse import Namespace
3
+ import asyncio
3
4
 
4
5
  from kmdr.core import *
5
6
  from kmdr.module import *
6
7
 
7
- def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
8
+ async def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
8
9
 
9
- if args.command == 'login':
10
- AUTHENTICATOR.get(args).authenticate()
10
+ if args.command == 'config':
11
+ CONFIGURER.get(args).operate()
12
+ return
11
13
 
12
- elif args.command == 'status':
13
- AUTHENTICATOR.get(args).authenticate()
14
+ async with KMDR_SESSION.get(args):
14
15
 
15
- elif args.command == 'download':
16
- AUTHENTICATOR.get(args).authenticate()
16
+ if args.command == 'login':
17
+ await AUTHENTICATOR.get(args).authenticate()
17
18
 
18
- book, volumes = LISTERS.get(args).list()
19
+ elif args.command == 'status':
20
+ await AUTHENTICATOR.get(args).authenticate()
19
21
 
20
- volumes = PICKERS.get(args).pick(volumes)
22
+ elif args.command == 'download':
23
+ await AUTHENTICATOR.get(args).authenticate()
21
24
 
22
- DOWNLOADER.get(args).download(book, volumes)
25
+ book, volumes = await LISTERS.get(args).list()
23
26
 
24
- elif args.command == 'config':
25
- CONFIGURER.get(args).operate()
27
+ volumes = PICKERS.get(args).pick(volumes)
28
+
29
+ await DOWNLOADER.get(args).download(book, volumes)
26
30
 
27
- else:
28
- fallback()
31
+ else:
32
+ fallback()
33
+
34
+ def main_sync(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
35
+ asyncio.run(main(args, fallback))
29
36
 
30
37
  def entry_point():
31
38
  try:
32
39
  parser = argument_parser()
33
40
  args = parser.parse_args()
34
- main(args, lambda: parser.print_help())
41
+
42
+ main_coro = main(args, lambda: parser.print_help())
43
+ asyncio.run(main_coro)
35
44
  except KeyboardInterrupt:
36
45
  print("\n操作已取消(KeyboardInterrupt)")
37
46
  exit(130)
@@ -1,8 +1,11 @@
1
1
  from typing import Optional
2
2
 
3
+ from yarl import URL
4
+
3
5
  from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
4
6
 
5
7
  from .utils import check_status
8
+ from .utils import PROFILE_URL
6
9
 
7
10
  @AUTHENTICATOR.register()
8
11
  class CookieAuthenticator(Authenticator):
@@ -14,15 +17,16 @@ class CookieAuthenticator(Authenticator):
14
17
  else:
15
18
  self._show_quota = False
16
19
 
17
- def _authenticate(self) -> bool:
20
+ async def _authenticate(self) -> bool:
18
21
  cookie = self._configurer.cookie
19
22
 
20
23
  if not cookie:
21
- raise LoginError("No cookie found, please login first.", ['kmdr login -u <username>'])
24
+ raise LoginError("无法找到 Cookie,请先完成登录。", ['kmdr login -u <username>'])
22
25
 
23
- self._session.cookies.update(cookie)
24
- return check_status(
26
+ self._session.cookie_jar.update_cookies(cookie, response_url=URL(PROFILE_URL))
27
+ return await check_status(
25
28
  self._session,
29
+ self._console,
26
30
  show_quota=self._show_quota,
27
31
  is_vip_setter=lambda value: setattr(self._profile, 'is_vip', value),
28
32
  level_setter=lambda value: setattr(self._profile, 'user_level', value),
@@ -1,6 +1,7 @@
1
1
  from typing import Optional
2
2
  import re
3
- from getpass import getpass
3
+
4
+ from rich.prompt import Prompt
4
5
 
5
6
  from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
6
7
 
@@ -25,32 +26,35 @@ class LoginAuthenticator(Authenticator):
25
26
  self._show_quota = show_quota
26
27
 
27
28
  if password is None:
28
- password = getpass("please input your password: ")
29
+ password = Prompt.ask("请输入密码", password=True, console=self._console)
29
30
 
30
31
  self._password = password
31
32
 
32
- def _authenticate(self) -> bool:
33
-
34
- response = self._session.post(
33
+ async def _authenticate(self) -> bool:
34
+
35
+ async with self._session.post(
35
36
  url = 'https://kox.moe/login_do.php',
36
37
  data = {
37
38
  'email': self._username,
38
39
  'passwd': self._password,
39
40
  'keepalive': 'on'
40
41
  },
41
- )
42
- response.raise_for_status()
43
- match = re.search(r'"\w+"', response.text)
44
-
45
- if not match:
46
- raise LoginError("Failed to extract authentication code from response.")
47
-
48
- code = match.group(0).split('"')[1]
49
- if code != CODE_OK:
50
- raise LoginError(f"Authentication failed with error code: {code} " + CODE_MAPPING.get(code, "Unknown error."))
51
-
52
- if check_status(self._session, show_quota=self._show_quota):
53
- self._configurer.cookie = self._session.cookies.get_dict()
54
- return True
55
-
56
- return False
42
+ ) as response:
43
+
44
+ response.raise_for_status()
45
+ match = re.search(r'"\w+"', await response.text())
46
+
47
+ if not match:
48
+ raise LoginError("无法解析登录响应。")
49
+
50
+ code = match.group(0).split('"')[1]
51
+ if code != CODE_OK:
52
+ raise LoginError(f"认证失败,错误代码:{code} " + CODE_MAPPING.get(code, "未知错误。"))
53
+
54
+ if await check_status(self._session, self._console, show_quota=self._show_quota):
55
+ cookie = self._session.cookie_jar.filter_cookies('https://kox.moe')
56
+ self._configurer.cookie = {key: morsel.value for key, morsel in cookie.items()}
57
+
58
+ return True
59
+
60
+ return False