kmoe-manga-downloader 1.0.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.
kmdr/__init__.py ADDED
File without changes
kmdr/core/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .bases import Authenticator, Lister, Picker, Downloader, Configurer
2
+ from .structure import VolInfo, BookInfo, VolumeType
3
+ from .bases import AUTHENTICATOR, LISTERS, PICKERS, DOWNLOADER, CONFIGURER
4
+
5
+ from .defaults import argument_parser
kmdr/core/bases.py ADDED
@@ -0,0 +1,113 @@
1
+ import os
2
+
3
+ from typing import Callable, Optional
4
+
5
+ from .registry import Registry
6
+ from .structure import VolInfo, BookInfo
7
+ from .utils import get_singleton_session, construct_callback
8
+ from .defaults import Configurer as InnerConfigurer
9
+
10
+ class SessionContext:
11
+
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__()
14
+ self._session = get_singleton_session()
15
+
16
+ class ConfigContext:
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__()
20
+ self._configurer = InnerConfigurer()
21
+
22
+ class Configurer(ConfigContext):
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+
27
+ def operate(self) -> None: ...
28
+
29
+ class Authenticator(SessionContext, ConfigContext):
30
+
31
+ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+
34
+ if proxy:
35
+ self._session.proxies.update({
36
+ 'https': proxy,
37
+ 'http': proxy,
38
+ })
39
+
40
+ # 在使用代理登录时,可能会出现问题,但是现在还不清楚是不是代理的问题。
41
+ # 主站正常情况下不使用代理也能登录成功。但是不排除特殊的网络环境下需要代理。
42
+ # 所以暂时保留代理登录的功能,如果后续确认是代理的问题,可以考虑启用 @no_proxy 装饰器。
43
+ # @no_proxy
44
+ def authenticate(self) -> bool:
45
+ return self._authenticate()
46
+
47
+ def _authenticate(self) -> bool: ...
48
+
49
+ class Lister(SessionContext):
50
+
51
+ def __init__(self, *args, **kwargs):
52
+ super().__init__(*args, **kwargs)
53
+
54
+ def list(self) -> tuple[BookInfo, list[VolInfo]]: ...
55
+
56
+ class Picker(SessionContext):
57
+
58
+ def __init__(self, *args, **kwargs):
59
+ super().__init__(*args, **kwargs)
60
+
61
+ def pick(self, volumes: list[VolInfo]) -> list[VolInfo]: ...
62
+
63
+ class Downloader(SessionContext):
64
+
65
+ def __init__(self,
66
+ dest: str = '.',
67
+ callback: Optional[str] = None,
68
+ retry: int = 3,
69
+ num_workers: int = 1,
70
+ proxy: Optional[str] = None,
71
+ *args, **kwargs
72
+ ):
73
+ super().__init__(*args, **kwargs)
74
+ self._dest: str = dest
75
+ self._callback: Optional[Callable[[BookInfo, VolInfo], int]] = construct_callback(callback)
76
+ self._retry: int = retry
77
+ self._num_workers: int = num_workers
78
+
79
+ if proxy:
80
+ self._session.proxies.update({
81
+ 'https': proxy,
82
+ 'http': proxy,
83
+ })
84
+
85
+ def download(self, book: BookInfo, volumes: list[VolInfo]):
86
+ if volumes is None or not volumes:
87
+ raise ValueError("No volumes to download")
88
+
89
+ if self._num_workers <= 1:
90
+ for volume in volumes:
91
+ self._download(book, volume, self._retry)
92
+ else:
93
+ self._download_with_multiple_workers(book, volumes, self._retry)
94
+
95
+ def _download(self, book: BookInfo, volume: VolInfo, retry: int): ...
96
+
97
+ def _download_with_multiple_workers(self, book: BookInfo, volumes: list[VolInfo], retry: int):
98
+ from concurrent.futures import ThreadPoolExecutor
99
+
100
+ max_workers = min(self._num_workers, len(volumes))
101
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
102
+ futures = [
103
+ executor.submit(self._download, book, volume, retry)
104
+ for volume in volumes
105
+ ]
106
+ for future in futures:
107
+ future.result()
108
+
109
+ AUTHENTICATOR = Registry[Authenticator]('Authenticator')
110
+ LISTERS = Registry[Lister]('Lister')
111
+ PICKERS = Registry[Picker]('Picker')
112
+ DOWNLOADER = Registry[Downloader]('Downloader', True)
113
+ CONFIGURER = Registry[Configurer]('Configurer')
kmdr/core/defaults.py ADDED
@@ -0,0 +1,154 @@
1
+ import os
2
+ import json
3
+ from typing import Optional
4
+ import argparse
5
+
6
+ from .utils import singleton
7
+ from .structure import Config
8
+
9
+ parser: Optional[argparse.ArgumentParser] = None
10
+ args: Optional[argparse.Namespace] = None
11
+
12
+ def argument_parser():
13
+ global parser
14
+ if parser is not None:
15
+ return parser
16
+
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')
44
+
45
+ return parser
46
+
47
+ def parse_args():
48
+ global args
49
+ if args is not None:
50
+ return args
51
+
52
+ parser = argument_parser()
53
+ args = parser.parse_args()
54
+
55
+ if args.command is None:
56
+ parser.print_help()
57
+ exit(1)
58
+
59
+ return args
60
+
61
+ @singleton
62
+ class Configurer:
63
+
64
+ def __init__(self):
65
+ self.__filename = '.kmdr'
66
+
67
+ if not os.path.exists(os.path.join(os.path.expanduser("~"), self.__filename)):
68
+ self._config = Config()
69
+ self.update()
70
+ else:
71
+ with open(os.path.join(os.path.expanduser("~"), self.__filename), 'r') as f:
72
+ config = json.load(f)
73
+
74
+ self._config = Config()
75
+ option = config.get('option', None)
76
+ if option is not None and isinstance(option, dict):
77
+ self._config.option = option
78
+ cookie = config.get('cookie', None)
79
+ if cookie is not None and isinstance(cookie, dict):
80
+ self._config.cookie = cookie
81
+
82
+ @property
83
+ def config(self) -> 'Config':
84
+ return self._config
85
+
86
+ @property
87
+ def cookie(self) -> Optional[dict]:
88
+ if self._config is None:
89
+ return None
90
+ return self._config.cookie
91
+
92
+ @cookie.setter
93
+ def cookie(self, value: Optional[dict[str, str]]):
94
+ if self._config is None:
95
+ self._config = Config()
96
+ self._config.cookie = value
97
+ self.update()
98
+
99
+ @property
100
+ def option(self) -> Optional[dict]:
101
+ if self._config is None:
102
+ return None
103
+ return self._config.option
104
+
105
+ @option.setter
106
+ def option(self, value: Optional[dict[str, any]]):
107
+ if self._config is None:
108
+ self._config = Config()
109
+ self._config.option = value
110
+ self.update()
111
+
112
+ def update(self):
113
+ with open(os.path.join(os.path.expanduser("~"), self.__filename), 'w') as f:
114
+ json.dump(self._config.__dict__, f, indent=4, ensure_ascii=False)
115
+
116
+ def clear(self, key: str):
117
+ if key == 'all':
118
+ self._config = Config()
119
+ elif key == 'cookie':
120
+ self._config.cookie = None
121
+ elif key == 'option':
122
+ self._config.option = None
123
+ else:
124
+ raise ValueError(f"Unsupported clear option: {key}")
125
+
126
+ self.update()
127
+
128
+ def set_option(self, key: str, value: any):
129
+ if self._config.option is None:
130
+ self._config.option = {}
131
+
132
+ self._config.option[key] = value
133
+ self.update()
134
+
135
+ def unset_option(self, key: str):
136
+ if self._config.option is None or key not in self._config.option:
137
+ return
138
+
139
+ del self._config.option[key]
140
+ self.update()
141
+
142
+ def __combine_args(dest: argparse.Namespace, option: dict) -> argparse.Namespace:
143
+ if option is None:
144
+ return dest
145
+
146
+ for key, value in option.items():
147
+ if hasattr(dest, key) and getattr(dest, key) is None:
148
+ setattr(dest, key, value)
149
+ return dest
150
+
151
+ def combine_args(dest: argparse.Namespace) -> argparse.Namespace:
152
+ assert isinstance(dest, argparse.Namespace), "dest must be an argparse.Namespace instance"
153
+ option = Configurer().config.option
154
+ return __combine_args(dest, option)
kmdr/core/registry.py ADDED
@@ -0,0 +1,128 @@
1
+ from typing import Optional, Callable, TypeVar, Generic
2
+ from dataclasses import dataclass, field
3
+ from argparse import Namespace
4
+
5
+ from .defaults import combine_args
6
+
7
+ T = TypeVar('T')
8
+
9
+ class Registry(Generic[T]):
10
+
11
+ def __init__(self, name: str, combine_args: bool = False):
12
+ self._name = name
13
+ self._modules: list['Predication'] = list()
14
+ self._combine_args = combine_args
15
+
16
+ def register(self,
17
+ hasattrs: frozenset[str] = frozenset(),
18
+ containattrs: frozenset[str] = frozenset(),
19
+ hasvalues: dict[str, object] = dict(),
20
+ predicate: Optional[Callable[[Namespace], bool]] = None,
21
+ order: int = 0,
22
+ name: Optional[str] = None
23
+ ):
24
+ """
25
+ 注册一个模块到注册表中。
26
+ 总体的匹配逻辑: `{predicate} or {hasvalues} and ({hasattrs} or {containattrs})`
27
+
28
+ :param hasattrs: 模块处理的参数集合,必须全部匹配。如果未提供,则从类的 __init__ 方法中获取不可缺省的参数
29
+ :param containattrs: 模块处理的可选参数集合,只要满足其中一个即可。
30
+ :param hasvalues: 模块处理的属性值集合,必须全部满足。
31
+ :param predicate: 可以提供预定义以外的条件,只要满足该条件就视为匹配。
32
+ :param order: 模块的优先级,数字越小优先级越高。
33
+ :param name: 模块的名称,如果未提供,则使用类名。
34
+ """
35
+
36
+ def wrapper(cls):
37
+ nonlocal hasattrs
38
+ nonlocal containattrs
39
+ nonlocal hasvalues
40
+ nonlocal name
41
+ nonlocal predicate
42
+
43
+ if name is None:
44
+ name = cls.__name__
45
+
46
+ if not hasattrs or len(hasattrs) == 0:
47
+ # 如果没有指定属性,则从类的 __init__ 方法中获取参数
48
+ if hasattr(cls, '__init__'):
49
+ init_signature = cls.__init__.__code__.co_varnames[1:cls.__init__.__code__.co_argcount]
50
+ init_defaults = cls.__init__.__defaults__ or ()
51
+ default_count = len(init_defaults)
52
+ required_params = init_signature[:len(init_signature) - default_count]
53
+ hasattrs = frozenset(required_params)
54
+ else:
55
+ raise ValueError(f'{self._name} requires at least one attribute to be specified for {name}')
56
+
57
+ predication = Predication(
58
+ cls=cls,
59
+ hasattrs=frozenset(hasattrs),
60
+ containattrs=frozenset(containattrs),
61
+ hasvalues=hasvalues,
62
+ predicate=predicate,
63
+ order=order
64
+ )
65
+
66
+ if predication in self._modules:
67
+ raise ValueError(f'{self._name} already has a module for {predication}')
68
+
69
+ self._modules.append(predication)
70
+ self._modules.sort()
71
+
72
+ return cls
73
+
74
+ return wrapper
75
+
76
+ def get(self, condition: Namespace) -> T:
77
+ if self._combine_args:
78
+ condition = combine_args(condition)
79
+ return self._get(condition)
80
+
81
+ def _get(self, condition: Namespace) -> T:
82
+ if not self._modules or len(self._modules) == 0:
83
+ raise ValueError(f'{self._name} has no registered modules')
84
+
85
+ if len(self._modules) == 1:
86
+ return self._modules[0].cls(**self._filter_nonone_args(condition))
87
+
88
+ for module in self._modules:
89
+ if (module.predicate is not None and module.predicate(condition)) or \
90
+ all(hasattr(condition, attr) and getattr(condition, attr) == value for attr, value in module.hasvalues.items()) and \
91
+ (all(hasattr(condition, attr) and getattr(condition, attr) is not None for attr in module.hasattrs) \
92
+ or any(hasattr(condition, attr) for attr in module.containattrs)):
93
+
94
+ # 手动配置的 predicate 优先级最高,只要满足 predicate 条件就返回
95
+ # hasvalues 配置的属性值必须完全匹配
96
+ # hasattrs 和 containattrs 二者只需要满足一个
97
+
98
+ return module.cls(**self._filter_nonone_args(condition))
99
+
100
+ raise ValueError(f'{self._name} does not have a module for {condition}')
101
+
102
+ def _filter_nonone_args(self, condition: Namespace) -> dict[str, object]:
103
+ return {k: v for k, v in vars(condition).items() if v is not None}
104
+
105
+ @dataclass(frozen=True)
106
+ class Predication:
107
+ cls: type
108
+
109
+ hasattrs: frozenset[str] = frozenset({})
110
+ containattrs: frozenset[str] = frozenset({})
111
+ hasvalues: dict[str, object] = field(default_factory=dict)
112
+ predicate: Optional[Callable[[Namespace], bool]] = None
113
+
114
+ order: int = 0
115
+
116
+ def __lt__(self, other: 'Predication') -> bool:
117
+ if self.order == other.order:
118
+ # 如果 order 相同,则比较 hasattrs 的长度
119
+ # 通常情况下,hasattrs 的长度越长,优先级越高
120
+ return len(self.hasattrs) > len(other.hasattrs)
121
+ return self.order < other.order
122
+
123
+ def __hash__(self) -> int:
124
+ return hash((self.cls, self.hasattrs, frozenset(self.hasvalues.items()), self.predicate, self.order))
125
+
126
+ def __eq__(self, other: 'Predication') -> bool:
127
+ return (self.cls, self.hasattrs, frozenset(self.hasvalues.items()), self.predicate, self.order) == \
128
+ (other.cls, other.hasattrs, frozenset(other.hasvalues.items()), other.predicate, other.order)
kmdr/core/structure.py ADDED
@@ -0,0 +1,68 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Optional
4
+
5
+ class VolumeType(Enum):
6
+ VOLUME = "單行本"
7
+ EXTRA = "番外篇"
8
+ SERIALIZED = "連載話"
9
+
10
+ @dataclass(frozen=True)
11
+ class VolInfo:
12
+ """
13
+ Kmoe 卷信息
14
+ """
15
+
16
+ id: str
17
+
18
+ extra_info: str
19
+ """
20
+ 额外信息
21
+ - 0: 无
22
+ - 1: 最近一週更新
23
+ - 2: 90天內曾下載/推送
24
+ """
25
+
26
+ is_last: bool
27
+
28
+ vol_type: VolumeType
29
+
30
+ index: int
31
+ """
32
+ 从1开始的卷索引
33
+ 如果卷类型为「連載話」,则表示起始话数
34
+ """
35
+
36
+ name: str
37
+
38
+ pages: int
39
+
40
+ size: float
41
+ """
42
+ 卷大小,单位为MB
43
+ """
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class BookInfo:
48
+ id: str
49
+ name: str
50
+ url: str
51
+ author: str
52
+ status: str
53
+ last_update: str
54
+
55
+ @dataclass
56
+ class Config:
57
+
58
+ option: Optional[dict] = None
59
+ """
60
+ 用来存储下载相关的配置选项
61
+ - retry_times: 重试次数
62
+ - dest: 下载文件保存路径
63
+ - callback: 下载完成后的回调函数
64
+ - proxy: 下载时使用的代理
65
+ - num_workers: 下载时使用的线程数
66
+ """
67
+
68
+ cookie: Optional[dict[str, str]] = None
kmdr/core/utils.py ADDED
@@ -0,0 +1,77 @@
1
+ import functools
2
+ from typing import Optional, Callable
3
+
4
+ from requests import Session
5
+ import threading
6
+ import subprocess
7
+
8
+ from .structure import BookInfo, VolInfo
9
+
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
+
36
+ def singleton(cls):
37
+ """
38
+ **非线程安全**的单例装饰器
39
+ """
40
+
41
+ instances = {}
42
+
43
+ def get_instance(*args, **kwargs):
44
+ if cls not in instances:
45
+ instances[cls] = cls(*args, **kwargs)
46
+ return instances[cls]
47
+
48
+ return get_instance
49
+
50
+ def construct_callback(callback: Optional[str]) -> Optional[Callable]:
51
+ if callback is None or not isinstance(callback, str) or not callback.strip():
52
+ return None
53
+
54
+ def _callback(book: BookInfo, volume: VolInfo) -> int:
55
+ nonlocal callback
56
+
57
+ assert callback, "Callback script cannot be empty"
58
+ formatted_callback = callback.strip().format(b=book, v=volume)
59
+
60
+ return subprocess.run(formatted_callback, shell=True, check=True).returncode
61
+
62
+ return _callback
63
+
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
+
77
+ return wrapper
kmdr/main.py ADDED
@@ -0,0 +1,39 @@
1
+ from typing import Callable
2
+ from argparse import Namespace
3
+
4
+ from kmdr.core import *
5
+ from kmdr.module import *
6
+
7
+ def main(args: Namespace, fallback: Callable[[], None] = lambda: print('NOT IMPLEMENTED!')) -> None:
8
+
9
+ if args.command == 'login':
10
+ if not AUTHENTICATOR.get(args).authenticate():
11
+ raise RuntimeError("Authentication failed. Please check your credentials.")
12
+
13
+ elif args.command == 'status':
14
+ if not AUTHENTICATOR.get(args).authenticate():
15
+ raise RuntimeError("Authentication failed. Please check your credentials.")
16
+
17
+ elif args.command == 'download':
18
+ if not AUTHENTICATOR.get(args).authenticate():
19
+ raise RuntimeError("Authentication failed. Please check your credentials.")
20
+
21
+ book, volumes = LISTERS.get(args).list()
22
+
23
+ volumes = PICKERS.get(args).pick(volumes)
24
+
25
+ DOWNLOADER.get(args).download(book, volumes)
26
+
27
+ elif args.command == 'config':
28
+ CONFIGURER.get(args).operate()
29
+
30
+ else:
31
+ fallback()
32
+
33
+ def entry_point():
34
+ parser = argument_parser()
35
+ args = parser.parse_args()
36
+ main(args, lambda: parser.print_help())
37
+
38
+ if __name__ == '__main__':
39
+ entry_point()
@@ -0,0 +1,5 @@
1
+ from .authenticator import CookieAuthenticator, LoginAuthenticator
2
+ from .lister import BookUrlLister, FollowedBookLister
3
+ from .picker import ArgsFilterPicker, DefaultVolPicker
4
+ from .downloader import DirectDownloader, ReferViaDownloader
5
+ from .configurer import OptionLister, OptionSetter, ConfigClearer, ConfigUnsetter
@@ -0,0 +1,25 @@
1
+ from typing import Optional
2
+
3
+ from kmdr.core import Authenticator, AUTHENTICATOR
4
+
5
+ from .utils import check_status
6
+
7
+ @AUTHENTICATOR.register()
8
+ class CookieAuthenticator(Authenticator):
9
+ def __init__(self, proxy: Optional[str] = None, *args, **kwargs):
10
+ super().__init__(proxy, *args, **kwargs)
11
+
12
+ if 'command' in kwargs and kwargs['command'] == 'status':
13
+ self._show_quota = True
14
+ else:
15
+ self._show_quota = False
16
+
17
+ def _authenticate(self) -> bool:
18
+ cookie = self._configurer.cookie
19
+
20
+ if not cookie:
21
+ print("No cookie found. Please login first.")
22
+ return False
23
+
24
+ self._session.cookies.update(cookie)
25
+ return check_status(self._session, show_quota=self._show_quota)
@@ -0,0 +1,54 @@
1
+ from typing import Optional
2
+ import re
3
+
4
+ from kmdr.core import Authenticator, AUTHENTICATOR
5
+
6
+ from .utils import check_status
7
+
8
+
9
+ @AUTHENTICATOR.register(
10
+ hasvalues = {'command': 'login'}
11
+ )
12
+ class LoginAuthenticator(Authenticator):
13
+ def __init__(self, username: str, proxy: Optional[str] = None, password: Optional[str] = None, show_quota = True, *args, **kwargs):
14
+ super().__init__(proxy, *args, **kwargs)
15
+ self._username = username
16
+ self._show_quota = show_quota
17
+
18
+ if password is None:
19
+ password = input("please input your password: \n")
20
+
21
+ self._password = password
22
+
23
+ def _authenticate(self) -> bool:
24
+
25
+ response = self._session.post(
26
+ url = 'https://kox.moe/login_do.php',
27
+ data = {
28
+ 'email': self._username,
29
+ 'passwd': self._password,
30
+ 'keepalive': 'on'
31
+ },
32
+ )
33
+ response.raise_for_status()
34
+
35
+ match = re.search('"\w+"', response.text)
36
+ 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)
49
+
50
+ if check_status(self._session, show_quota=self._show_quota):
51
+ self._configurer.cookie = self._session.cookies.get_dict()
52
+ return True
53
+
54
+ return False