Skip to content

Commit e22790f

Browse files
authored
Merge pull request #10 from chrisis58/dev
fix: 修复部分存在的问题,优化使用体验
2 parents 42aaf8d + 6a40aa3 commit e22790f

File tree

15 files changed

+315
-75
lines changed

15 files changed

+315
-75
lines changed

README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ pip install kmoe-manga-downloader
2828

2929
```bash
3030
kmdr login -u <your_username> -p <your_password>
31-
```
32-
33-
或者:
34-
35-
```bash
31+
# 或者
3632
kmdr login -u <your_username>
3733
```
3834

39-
第二种方式会在程序运行时获取登录密码。如果登录成功,会同时显示当前登录用户及配额。
35+
第二种方式会在程序运行时获取登录密码,此时你输入的密码**不会显示**在终端中。
36+
37+
如果登录成功,会同时显示当前登录用户及配额。
4038

4139
### 2. 下载漫画书籍
4240

src/kmdr/core/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
from .structure import VolInfo, BookInfo, VolumeType
33
from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER
44

5-
from .defaults import argument_parser
5+
from .defaults import argument_parser
6+
7+
from .error import KmdrError, LoginError

src/kmdr/core/bases.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@
22

33
from typing import Callable, Optional
44

5+
from .error import LoginError
56
from .registry import Registry
67
from .structure import VolInfo, BookInfo
78
from .utils import get_singleton_session, construct_callback
8-
from .defaults import Configurer as InnerConfigurer
9+
from .defaults import Configurer as InnerConfigurer, UserProfile
910

1011
class SessionContext:
1112

1213
def __init__(self, *args, **kwargs):
1314
super().__init__()
1415
self._session = get_singleton_session()
1516

17+
class UserProfileContext:
18+
19+
def __init__(self, *args, **kwargs):
20+
super().__init__()
21+
self._profile = UserProfile()
22+
1623
class ConfigContext:
1724

1825
def __init__(self, *args, **kwargs):
@@ -26,7 +33,7 @@ def __init__(self, *args, **kwargs):
2633

2734
def operate(self) -> None: ...
2835

29-
class Authenticator(SessionContext, ConfigContext):
36+
class Authenticator(SessionContext, ConfigContext, UserProfileContext):
3037

3138
def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
3239
super().__init__(*args, **kwargs)
@@ -41,8 +48,13 @@ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
4148
# 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
4249
# 所以暂时保留代理登录的功能,如果后续确认是代理的问题,可以考虑启用 @no_proxy 装饰器。
4350
# @no_proxy
44-
def authenticate(self) -> bool:
45-
return self._authenticate()
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)
4658

4759
def _authenticate(self) -> bool: ...
4860

@@ -60,7 +72,7 @@ def __init__(self, *args, **kwargs):
6072

6173
def pick(self, volumes: list[VolInfo]) -> list[VolInfo]: ...
6274

63-
class Downloader(SessionContext):
75+
class Downloader(SessionContext, UserProfileContext):
6476

6577
def __init__(self,
6678
dest: str = '.',

src/kmdr/core/defaults.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ def parse_args():
5858

5959
return args
6060

61+
@singleton
62+
class UserProfile:
63+
64+
def __init__(self):
65+
self._is_vip: Optional[int] = None
66+
self._user_level: Optional[int] = None
67+
68+
@property
69+
def is_vip(self) -> Optional[int]:
70+
return self._is_vip
71+
72+
@property
73+
def user_level(self) -> Optional[int]:
74+
return self._user_level
75+
76+
@is_vip.setter
77+
def is_vip(self, value: Optional[int]):
78+
self._is_vip = value
79+
80+
@user_level.setter
81+
def user_level(self, value: Optional[int]):
82+
self._user_level = value
83+
6184
@singleton
6285
class Configurer:
6386

src/kmdr/core/error.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Optional
2+
3+
class KmdrError(RuntimeError):
4+
def __init__(self, message: str, solution: Optional[list[str]] = None, *args: object, **kwargs: object):
5+
super().__init__(message, *args, **kwargs)
6+
self.message = message
7+
8+
self._solution = "" if solution is None else "\nSuggested Solution: \n" + "\n".join(f">>> {sol}" for sol in solution)
9+
10+
class LoginError(KmdrError):
11+
def __init__(self, message, solution: Optional[list[str]] = None):
12+
super().__init__(message, solution)
13+
14+
def __str__(self):
15+
return f"{self.message}\n{self._solution}"

src/kmdr/main.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
88

99
if args.command == 'login':
10-
if not AUTHENTICATOR.get(args).authenticate():
11-
raise RuntimeError("Authentication failed. Please check your credentials.")
10+
AUTHENTICATOR.get(args).authenticate()
1211

1312
elif args.command == 'status':
14-
if not AUTHENTICATOR.get(args).authenticate():
15-
raise RuntimeError("Authentication failed. Please check your credentials.")
13+
AUTHENTICATOR.get(args).authenticate()
1614

17-
elif args.command == 'download':
18-
if not AUTHENTICATOR.get(args).authenticate():
19-
raise RuntimeError("Authentication failed. Please check your credentials.")
15+
elif args.command == 'download':
16+
AUTHENTICATOR.get(args).authenticate()
2017

2118
book, volumes = LISTERS.get(args).list()
2219

src/kmdr/module/authenticator/CookieAuthenticator.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22

3-
from kmdr.core import Authenticator, AUTHENTICATOR
3+
from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
44

55
from .utils import check_status
66

@@ -18,8 +18,12 @@ def _authenticate(self) -> bool:
1818
cookie = self._configurer.cookie
1919

2020
if not cookie:
21-
print("No cookie found. Please login first.")
22-
return False
21+
raise LoginError("No cookie found, please login first.", ['kmdr login -u <username>'])
2322

2423
self._session.cookies.update(cookie)
25-
return check_status(self._session, show_quota=self._show_quota)
24+
return check_status(
25+
self._session,
26+
show_quota=self._show_quota,
27+
is_vip_setter=lambda value: setattr(self._profile, 'is_vip', value),
28+
level_setter=lambda value: setattr(self._profile, 'user_level', value),
29+
)

src/kmdr/module/authenticator/LoginAuthenticator.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from typing import Optional
22
import re
3+
from getpass import getpass
34

4-
from kmdr.core import Authenticator, AUTHENTICATOR
5+
from kmdr.core import Authenticator, AUTHENTICATOR, LoginError
56

67
from .utils import check_status
78

9+
CODE_OK = 'm100'
10+
11+
CODE_MAPPING = {
12+
'e400': "帳號或密碼錯誤。",
13+
'e401': "非法訪問,請使用瀏覽器正常打開本站",
14+
'e402': "帳號已經註銷。不會解釋原因,無需提問。",
15+
'e403': "驗證失效,請刷新頁面重新操作。",
16+
}
817

918
@AUTHENTICATOR.register(
1019
hasvalues = {'command': 'login'}
@@ -16,7 +25,7 @@ def __init__(self, username: str, proxy: Optional[str] = None, password: Optiona
1625
self._show_quota = show_quota
1726

1827
if password is None:
19-
password = input("please input your password: \n")
28+
password = getpass("please input your password: ")
2029

2130
self._password = password
2231

@@ -31,22 +40,15 @@ def _authenticate(self) -> bool:
3140
},
3241
)
3342
response.raise_for_status()
34-
35-
match = re.search('"\w+"', response.text)
43+
match = re.search(r'"\w+"', response.text)
44+
3645
if not match:
37-
raise RuntimeError("Failed to extract authentication code from response.")
38-
code = match.group(0).split('"')[1]
39-
if code != 'm100':
40-
if code == 'e400':
41-
print("帳號或密碼錯誤。")
42-
elif code == 'e401':
43-
print("非法訪問,請使用瀏覽器正常打開本站")
44-
elif code == 'e402':
45-
print("帳號已經註銷。不會解釋原因,無需提問。")
46-
elif code == 'e403':
47-
print("驗證失效,請刷新頁面重新操作。")
48-
raise RuntimeError("Authentication failed with code: " + code)
46+
raise LoginError("Failed to extract authentication code from response.")
4947

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+
5052
if check_status(self._session, show_quota=self._show_quota):
5153
self._configurer.cookie = self._session.cookies.get_dict()
5254
return True
Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,79 @@
1+
from typing import Optional, Callable
2+
13
from requests import Session
24

3-
def check_status(session: Session, show_quota: bool = False) -> bool:
4-
response = session.get(url = 'https://kox.moe/my.php')
5+
from kmdr.core.error import LoginError
6+
7+
PROFILE_URL = 'https://kox.moe/my.php'
8+
LOGIN_URL = 'https://kox.moe/login.php'
9+
10+
NICKNAME_ID = 'div_nickname_display'
11+
12+
VIP_ID = 'div_user_vip'
13+
NOR_ID = 'div_user_nor'
14+
LV1_ID = 'div_user_lv1'
15+
16+
def check_status(
17+
session: Session,
18+
show_quota: bool = False,
19+
is_vip_setter: Optional[Callable[[int], None]] = None,
20+
level_setter: Optional[Callable[[int], None]] = None
21+
) -> bool:
22+
response = session.get(url = PROFILE_URL)
523

624
try:
725
response.raise_for_status()
826
except Exception as e:
927
print(f"Error: {type(e).__name__}: {e}")
1028
return False
29+
30+
if response.history and any(resp.status_code in (301, 302, 307) for resp in response.history) \
31+
and response.url == LOGIN_URL:
32+
raise LoginError("Invalid credentials, please login again.", ['kmdr config -c cookie', 'kmdr login -u <username>'])
1133

12-
if not show_quota:
34+
if not is_vip_setter and not level_setter and not show_quota:
1335
return True
1436

1537
from bs4 import BeautifulSoup
1638

1739
soup = BeautifulSoup(response.text, 'html.parser')
40+
41+
script = soup.find('script', language="javascript")
42+
43+
if script:
44+
var_define = extract_var_define(script.text[:100])
45+
46+
is_vip = int(var_define.get('is_vip', '0'))
47+
user_level = int(var_define.get('user_level', '0'))
48+
49+
if is_vip_setter:
50+
is_vip_setter(is_vip)
51+
if level_setter:
52+
level_setter(user_level)
1853

19-
nickname = soup.find('div', id='div_nickname_display').text.strip().split(' ')[0]
20-
print(f"=========================\n\nLogged in as {nickname}\n\n=========================\n")
21-
22-
quota = soup.find('div', id='div_user_vip').text.strip()
23-
print(f"=========================\n\n{quota}\n\n=========================\n")
54+
if not show_quota:
55+
return True
56+
57+
nickname = soup.find('div', id=NICKNAME_ID).text.strip().split(' ')[0]
58+
quota = soup.find('div', id=__resolve_quota_id(is_vip, user_level)).text.strip()
59+
60+
print(f"\n当前登录为 {nickname}\n\n{quota}")
2461
return True
62+
63+
def extract_var_define(script_text) -> dict[str, str]:
64+
var_define = {}
65+
for line in script_text.splitlines():
66+
line = line.strip()
67+
if line.startswith("var ") and "=" in line:
68+
var_name, var_value = line[4:].split("=", 1)
69+
var_define[var_name.strip()] = var_value.strip().strip(";").strip('"')
70+
return var_define
71+
72+
def __resolve_quota_id(is_vip: Optional[int] = None, user_level: Optional[int] = None):
73+
if is_vip is not None and is_vip >= 1:
74+
return VIP_ID
75+
76+
if user_level is not None and user_level <= 1:
77+
return LV1_ID
2578

79+
return NOR_ID

src/kmdr/module/configurer/ConfigUnsetter.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from kmdr.core import Configurer, CONFIGURER
22

3+
from .option_validate import check_key
4+
35
@CONFIGURER.register()
46
class ConfigUnsetter(Configurer):
57
def __init__(self, unset: str, *args, **kwargs):
@@ -10,6 +12,7 @@ def operate(self) -> None:
1012
if not self._unset:
1113
print("No option specified to unset.")
1214
return
13-
15+
16+
check_key(self._unset)
1417
self._configurer.unset_option(self._unset)
1518
print(f"Unset configuration: {self._unset}")

0 commit comments

Comments
 (0)