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
@@ -12,13 +12,15 @@ import aiofiles.os as aio_os
12
12
  from rich.progress import Progress
13
13
  from aiohttp.client_exceptions import ClientPayloadError
14
14
 
15
+ from kmdr.core.console import info, log, debug
16
+
15
17
  from .misc import STATUS, StateManager
16
18
 
17
19
  BLOCK_SIZE_REDUCTION_FACTOR = 0.75
18
20
  MIN_BLOCK_SIZE = 2048
19
21
 
20
22
 
21
- @deprecated("请使用 'download_file_multipart'")
23
+ @deprecated("本函数可能不会积极维护,请改用 'download_file_multipart'")
22
24
  async def download_file(
23
25
  session: aiohttp.ClientSession,
24
26
  semaphore: asyncio.Semaphore,
@@ -53,9 +55,11 @@ async def download_file(
53
55
  await aio_os.makedirs(dest_path, exist_ok=True)
54
56
 
55
57
  if await aio_os.path.exists(file_path):
56
- progress.console.print(f"[yellow]{filename} 已经存在[/yellow]")
58
+ info(f"[yellow]{filename} 已经存在[/yellow]")
57
59
  return
58
60
 
61
+ log("开始下载文件:", filename, "到路径:", dest_path)
62
+
59
63
  block_size = 8192
60
64
  attempts_left = retry_times + 1
61
65
  task_id = None
@@ -105,7 +109,7 @@ async def download_file(
105
109
  else:
106
110
  raise IOError(f"Failed to download {filename} after {retry_times} retries.")
107
111
 
108
- os.rename(filename_downloading, file_path)
112
+ await aio_os.rename(filename_downloading, file_path)
109
113
 
110
114
  except Exception as e:
111
115
  if task_id is not None:
@@ -154,16 +158,23 @@ async def download_file_multipart(
154
158
  await aio_os.makedirs(dest_path, exist_ok=True)
155
159
 
156
160
  if await aio_os.path.exists(file_path):
157
- progress.console.print(f"[blue]{filename} 已经存在[/blue]")
161
+ info(f"[blue]{filename} 已经存在[/blue]")
158
162
  return
159
163
 
164
+ log(f"开始下载文件: {filename} 到路径: {dest_path}")
165
+
160
166
  part_paths = []
161
167
  part_expected_sizes = []
162
168
  task_id = None
169
+
170
+ state_manager: Optional[StateManager] = None
163
171
  try:
164
172
  current_url = await fetch_url(url)
165
173
 
166
174
  async with session.head(current_url, headers=headers, allow_redirects=True) as response:
175
+ # 注意:这个请求完成后,服务器就会记录这次下载,并消耗对应的流量配额,详细的规则请参考网站说明:
176
+ # 注 1 : 訂閱連載中的漫畫,有更新時自動推送的卷(冊),暫不計算在使用額度中,不扣減使用額度。
177
+ # 注 2 : 對同一卷(冊)書在 12 小時內重複*下載*,不會重複扣減額度。但重復推送是會扣減的。
167
178
  response.raise_for_status()
168
179
  total_size = int(response.headers['Content-Length'])
169
180
 
@@ -206,14 +217,14 @@ async def download_file_multipart(
206
217
  if all(results):
207
218
  await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.MERGING)
208
219
  await _merge_parts(part_paths, filename_downloading)
209
- os.rename(filename_downloading, file_path)
220
+ await aio_os.rename(filename_downloading, file_path)
210
221
  else:
211
222
  # 如果有任何一个分片校验失败,则视为下载失败
212
223
  await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.FAILED)
213
224
 
214
225
  finally:
215
226
  if await aio_os.path.exists(file_path):
216
- if task_id is not None:
227
+ if task_id is not None and state_manager is not None:
217
228
  await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.COMPLETED)
218
229
 
219
230
  cleanup_tasks = [aio_os.remove(p) for p in part_paths if await aio_os.path.exists(p)]
@@ -222,7 +233,7 @@ async def download_file_multipart(
222
233
  if callback:
223
234
  callback()
224
235
  else:
225
- if task_id is not None:
236
+ if task_id is not None and state_manager is not None:
226
237
  await state_manager.request_status_update(part_id=StateManager.PARENT_ID, status=STATUS.FAILED)
227
238
 
228
239
  async def _download_part(
@@ -256,6 +267,7 @@ async def _download_part(
256
267
  local_headers['Range'] = f'bytes={current_start}-{end}'
257
268
 
258
269
  async with semaphore:
270
+ debug("开始下载分片:", os.path.basename(part_path), "范围:", current_start, "-", end)
259
271
  async with session.get(url, headers=local_headers) as response:
260
272
  response.raise_for_status()
261
273
 
@@ -266,13 +278,22 @@ async def _download_part(
266
278
  if chunk:
267
279
  await f.write(chunk)
268
280
  state_manager.advance(len(chunk))
281
+ log("分片", os.path.basename(part_path), "下载完成。")
269
282
  return
283
+
284
+ except asyncio.CancelledError:
285
+ # 如果任务被取消,更新状态为已取消
286
+ await state_manager.request_status_update(part_id=start, status=STATUS.CANCELLED)
287
+ raise
288
+
270
289
  except Exception as e:
271
290
  if attempts_left > 0:
291
+ debug("分片", os.path.basename(part_path), "下载出错:", e, ",正在重试... 剩余重试次数:", attempts_left)
272
292
  await asyncio.sleep(3)
273
293
  await state_manager.request_status_update(part_id=start, status=STATUS.WAITING)
274
294
  else:
275
295
  # console.print(f"[red]分片 {os.path.basename(part_path)} 下载失败: {e}[/red]")
296
+ debug("分片", os.path.basename(part_path), "下载失败:", e)
276
297
  await state_manager.request_status_update(part_id=start, status=STATUS.PARTIALLY_FAILED)
277
298
 
278
299
  async def _validate_part(part_path: str, expected_size: int) -> bool:
@@ -282,6 +303,7 @@ async def _validate_part(part_path: str, expected_size: int) -> bool:
282
303
  return actual_size == expected_size
283
304
 
284
305
  async def _merge_parts(part_paths: list[str], final_path: str):
306
+ debug("合并分片到最终文件:", final_path)
285
307
  async with aiofiles.open(final_path, 'wb') as final_file:
286
308
  try:
287
309
  for part_path in part_paths:
@@ -297,7 +319,31 @@ async def _merge_parts(part_paths: list[str], final_path: str):
297
319
  raise e
298
320
 
299
321
 
322
+ CHAR_MAPPING = {
323
+ '\\': '\',
324
+ '/': '/',
325
+ ':': ':',
326
+ '*': '*',
327
+ '?': '?',
328
+ '"': '"',
329
+ '<': '<',
330
+ '>': '>',
331
+ '|': '|',
332
+ }
333
+ DEFAULT_ILLEGAL_CHARS_REPLACEMENT = '_'
334
+ ILLEGAL_CHARS_RE = re.compile(r'[\\/:*?"<>|]')
335
+
336
+ def readable_safe_filename(name: str) -> str:
337
+ """
338
+ 将字符串转换为安全的文件名,替换掉非法字符。
339
+ """
340
+ def replace_char(match):
341
+ char = match.group(0)
342
+ return CHAR_MAPPING.get(char, DEFAULT_ILLEGAL_CHARS_REPLACEMENT)
343
+
344
+ return ILLEGAL_CHARS_RE.sub(replace_char, name).strip()
300
345
 
346
+ @deprecated("请使用 'readable_safe_filename'")
301
347
  def safe_filename(name: str) -> str:
302
348
  """
303
349
  替换非法文件名字符为下划线
@@ -12,6 +12,7 @@ class STATUS(Enum):
12
12
  COMPLETED='[green]完成[/green]'
13
13
  PARTIALLY_FAILED='[red]分片失败[/red]'
14
14
  FAILED='[red]失败[/red]'
15
+ CANCELLED='[yellow]已取消[/yellow]'
15
16
 
16
17
  @property
17
18
  def order(self) -> int:
@@ -23,6 +24,7 @@ class STATUS(Enum):
23
24
  STATUS.COMPLETED: 5,
24
25
  STATUS.PARTIALLY_FAILED: 6,
25
26
  STATUS.FAILED: 7,
27
+ STATUS.CANCELLED: 8,
26
28
  }
27
29
  return order_mapping[self]
28
30
 
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- from urllib.parse import urljoin
3
2
 
4
3
  from bs4 import BeautifulSoup
5
4
  from rich.table import Table
@@ -8,6 +7,7 @@ from rich.prompt import IntPrompt
8
7
  from kmdr.core import Lister, LISTERS, BookInfo, VolInfo
9
8
  from kmdr.core.utils import async_retry
10
9
  from kmdr.core.constants import API_ROUTE
10
+ from kmdr.core.console import info
11
11
 
12
12
  from .utils import extract_book_info_and_volumes
13
13
 
@@ -24,7 +24,7 @@ class FollowedBookLister(Lister):
24
24
  books = await self._list_followed_books()
25
25
 
26
26
  if not books:
27
- self._console.print("[yellow]关注列表为空。[/yellow]")
27
+ info("[yellow]关注列表为空。[/yellow]")
28
28
  exit(0)
29
29
 
30
30
  table = Table(title="关注的书籍列表", show_header=True, header_style="bold blue")
@@ -43,7 +43,7 @@ class FollowedBookLister(Lister):
43
43
  book.status
44
44
  )
45
45
 
46
- self._console.print(table)
46
+ info(table)
47
47
 
48
48
  valid_choices = [str(i) for i in range(1, len(books) + 1)]
49
49
 
@@ -63,7 +63,7 @@ class FollowedBookLister(Lister):
63
63
 
64
64
  @async_retry()
65
65
  async def _list_followed_books(self) -> 'list[BookInfo]':
66
- async with self._session.get(urljoin(self._base_url, API_ROUTE.MY_FOLLOW)) as response:
66
+ async with self._session.get(API_ROUTE.MY_FOLLOW) as response:
67
67
  response.raise_for_status()
68
68
  html_text = await response.text()
69
69
 
@@ -1,13 +1,13 @@
1
1
  from bs4 import BeautifulSoup
2
2
  import re
3
3
  from typing import Optional
4
- from urllib.parse import urljoin
5
4
 
6
5
  from yarl import URL
7
6
  from aiohttp import ClientSession as Session
8
7
 
9
8
  from kmdr.core import BookInfo, VolInfo, VolumeType
10
9
  from kmdr.core.utils import async_retry
10
+ from kmdr.core.console import debug
11
11
 
12
12
  @async_retry()
13
13
  async def extract_book_info_and_volumes(session: Session, url: str, book_info: Optional[BookInfo] = None) -> tuple[BookInfo, list[VolInfo]]:
@@ -20,19 +20,21 @@ async def extract_book_info_and_volumes(session: Session, url: str, book_info: O
20
20
  """
21
21
  structured_url = URL(url)
22
22
 
23
+ # 移除移动端路径部分,统一为桌面端路径
24
+ # 因为移动端页面的结构与桌面端不同,可能会影响解析
25
+ route = structured_url.path
23
26
  if structured_url.path.startswith('/m/'):
24
- # 移除移动端路径部分,统一为桌面端路径
25
- # 因为移动端页面的结构与桌面端不同,可能会影响解析
26
- structured_url = structured_url.with_path(structured_url.path.replace('/m/', '', 1))
27
+ debug("检测到移动端链接,转换为桌面端链接进行处理。")
28
+ route = structured_url.path[2:]
27
29
 
28
- async with session.get(structured_url) as response:
30
+ async with session.get(route) as response:
29
31
  response.raise_for_status()
30
32
 
31
33
  # 如果后续有性能问题,可以先考虑使用 lxml 进行解析
32
34
  book_page = BeautifulSoup(await response.text(), 'html.parser')
33
35
 
34
36
  book_info = __extract_book_info(url, book_page, book_info)
35
- volumes = await __extract_volumes(session, url, book_page)
37
+ volumes = await __extract_volumes(session, book_page)
36
38
 
37
39
  return book_info, volumes
38
40
 
@@ -51,13 +53,13 @@ def __extract_book_info(url: str, book_page: BeautifulSoup, book_info: Optional[
51
53
  )
52
54
 
53
55
 
54
- async def __extract_volumes(session: Session, url: str, book_page: BeautifulSoup) -> list[VolInfo]:
56
+ async def __extract_volumes(session: Session, book_page: BeautifulSoup) -> list[VolInfo]:
55
57
  script = book_page.find_all('script', language="javascript")[-1].text
56
58
 
57
59
  pattern = re.compile(r'/book_data.php\?h=\w+')
58
60
  book_data_url = pattern.search(script).group(0)
59
61
 
60
- async with session.get(url = urljoin(url, book_data_url)) as response:
62
+ async with session.get(url = book_data_url) as response:
61
63
  response.raise_for_status()
62
64
 
63
65
  book_data = (await response.text()).split('\n')
@@ -2,6 +2,7 @@ from rich.table import Table
2
2
  from rich.prompt import Prompt
3
3
 
4
4
  from kmdr.core import Picker, PICKERS, VolInfo
5
+ from kmdr.core.console import info
5
6
 
6
7
  from .utils import resolve_volume
7
8
 
@@ -35,7 +36,7 @@ class DefaultVolPicker(Picker):
35
36
  f"{volume.size:.2f}"
36
37
  )
37
38
 
38
- self._console.print(table)
39
+ info(table)
39
40
 
40
41
  choice_str = Prompt.ask(
41
42
  "[green]请选择要下载的卷序号 (例如 'all', '1,2,3', '1-3,4-6')[/green]",
@@ -1,5 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
+ from kmdr.core.error import ArgsResolveError
4
+
3
5
  def resolve_volume(volume: str) -> Optional[set[int]]:
4
6
  if volume == 'all':
5
7
  return None
@@ -18,20 +20,26 @@ def resolve_volume(volume: str) -> Optional[set[int]]:
18
20
 
19
21
  if (volume := volume.strip()).isdigit():
20
22
  # 只有一个数字
21
- assert (volume := int(volume)) > 0, "Volume number must be greater than 0."
22
- return {volume}
23
+ if (volume_digit := int(volume)) <= 0:
24
+ raise ArgsResolveError(f"卷号必须大于 0,当前值为 {volume_digit}。")
25
+ return {volume_digit}
23
26
  elif '-' in volume and volume.count('-') == 1 and ',' not in volume:
24
27
  # 使用了范围符号
25
28
  start, end = volume.split('-')
26
29
 
27
- assert start.strip().isdigit() and end.strip().isdigit(), "Invalid range format. Use 'start-end' or 'start, end'."
30
+ if not start.strip().isdigit() or not end.strip().isdigit():
31
+ raise ArgsResolveError(f"无效的范围格式: {volume}。请使用 'start-end' 或 'start, end'。")
28
32
 
29
33
  start = int(start.strip())
30
34
  end = int(end.strip())
31
35
 
32
- assert start > 0 and end > 0, "Volume numbers must be greater than 0."
33
- assert start <= end, "Start of range must be less than or equal to end."
36
+ if start <= 0:
37
+ raise ArgsResolveError(f"卷号必须大于 0,当前值为 {start}。")
38
+ if end <= 0:
39
+ raise ArgsResolveError(f"卷号必须大于 0,当前值为 {end}。")
40
+ if start > end:
41
+ raise ArgsResolveError(f"起始卷号必须小于或等于结束卷号,当前值为 {start} - {end}。")
34
42
 
35
43
  return set(range(start, end + 1))
36
44
 
37
- raise ValueError(f"Invalid volume format: {volume}. Use 'all', '1,2,3', '1-3', or '1-3,4-6'.")
45
+ raise ArgsResolveError(f"无效的卷号格式: {volume}。请使用 'all', '1,2,3', '1-3', '1-3,4-6'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kmoe-manga-downloader
3
- Version: 1.2.1
3
+ Version: 1.2.3b0
4
4
  Summary: A CLI-downloader for site @kox.moe.
5
5
  Author-email: Chris Zheng <chrisis58@outlook.com>
6
6
  License: MIT License
@@ -0,0 +1,46 @@
1
+ kmdr/__init__.py,sha256=iLmy2rOkHS_4KZWMD8BgT7R3tLMKeaTCDVf3B4FyYxM,91
2
+ kmdr/_version.py,sha256=wqiogbpHngoX8QtZ_nlsUei1NMVQXPtFkYVQOrBjkgo,712
3
+ kmdr/main.py,sha256=xNUeU1voEMEqlf8z14EyiV-j61HPsRA3t2Nzreoaib4,2227
4
+ kmdr/core/__init__.py,sha256=aR3mhP1A81oTmTnBOonVQm_mFvFeNCOo7kalZVfszAc,416
5
+ kmdr/core/bases.py,sha256=br3HJC4mWL2jN4Y5QeXB5SwA0xnOfi2ougr5pEx7j8s,3989
6
+ kmdr/core/console.py,sha256=VziwGJQygF-zCrzTApPF-I9BgZKFSqCTiZ_pc5n8C-4,1950
7
+ kmdr/core/constants.py,sha256=JFj8FHO9ab6AeIzHz3LaC8l3ziLLFi75vil8wMPfZoA,1869
8
+ kmdr/core/context.py,sha256=IU5zHALsl4s9FeQJukhs703RAUe57Qf5P-nUAqGcW2A,1368
9
+ kmdr/core/defaults.py,sha256=tF41i145znhhDGVNL5irQpdF4lV_1tkWtvToRfm2R4s,8968
10
+ kmdr/core/error.py,sha256=qdbMEU6YhRcdbxlfZKKydoMFYXOCJQ473EYzl3mO33Q,1585
11
+ kmdr/core/protocol.py,sha256=ntHmnrvg_nZXh2-It0Wu9g2e8f3YpOLfpoRsWniG8as,247
12
+ kmdr/core/registry.py,sha256=KYjvww5WRuUg5SeIeWZb96803-kgrTIKYvFTj_7bPfo,5560
13
+ kmdr/core/session.py,sha256=ZrnoxZWUwLSMiLIOZDGEQZcH7RuBLcc5e2E0m17qX-s,4937
14
+ kmdr/core/structure.py,sha256=EQG1-8kHQ22Not2S7Q_jGxE0nb9XUbLh3JcNTb_nkvk,1199
15
+ kmdr/core/utils.py,sha256=_tRLggprXTGFvbRzyt3dgapQr2wI5NQ8Hc4_BHVj7lU,4069
16
+ kmdr/module/__init__.py,sha256=hY6lMBpxSn93t7OKnEbXUwTok2XqQoQbuyq1HLeC4Kc,125
17
+ kmdr/module/authenticator/CookieAuthenticator.py,sha256=lv_monQ7QUMfgPPgxoV1xr_KyUtwCKZhqJ8umv3W6xo,1108
18
+ kmdr/module/authenticator/LoginAuthenticator.py,sha256=5nyrzxYEGiRQ9ulnhjpuUWYeog-ALZlCCSAYkbwlKMw,1890
19
+ kmdr/module/authenticator/__init__.py,sha256=iSWhq-suAM1BrABRuTWiCKvZ1fkmf5HQ2GmbZ341GK0,103
20
+ kmdr/module/authenticator/utils.py,sha256=b49cwKjHVqOIHei0SUU5kMtWAgkYU0NH3YvDvdPhkxM,3053
21
+ kmdr/module/configurer/BaseUrlUpdator.py,sha256=OsdMUuyGujfqE6vEPt96sCJkJTfsPxAf5z2hbjcNZsQ,522
22
+ kmdr/module/configurer/ConfigClearer.py,sha256=irGBsck1EhTWl4IIOPF5b4o4TyrGvDAzibg9reQsI9A,501
23
+ kmdr/module/configurer/ConfigUnsetter.py,sha256=tbrOGKnTSeKhdMukIBPFqtdctEiQhu7GRThLthFYZew,602
24
+ kmdr/module/configurer/OptionLister.py,sha256=hqOtXfPcunzqkrq6Qx1KyZwEivp_vbpTVZ8PP3Q0MbY,1471
25
+ kmdr/module/configurer/OptionSetter.py,sha256=BQXIgyLoMDSNua2x96FtX-9JmJhVpPJ5o0tWHj_-7B0,898
26
+ kmdr/module/configurer/__init__.py,sha256=nSWGwUCcwOkvg28zxRd0S3b4jeag_W0IhzP74xq0ewE,204
27
+ kmdr/module/configurer/option_validate.py,sha256=tb6fvj01cDiPTQa6tC4-dYcTfUJUX3DBmkHKAKeuW3g,3358
28
+ kmdr/module/downloader/DirectDownloader.py,sha256=SPnzKDpVPg3utyrPrFr4pYiMI5w3eRCMemcYevH9VzY,1320
29
+ kmdr/module/downloader/ReferViaDownloader.py,sha256=o565u6W4G_4UFoqsLkXGRFIlRSMueZB_ZMdiAfhUVCc,1970
30
+ kmdr/module/downloader/__init__.py,sha256=PajPBQ4ZJwz_6Ok6k-HV7-YSibxAW5m5voYyP-U-_f4,97
31
+ kmdr/module/downloader/download_utils.py,sha256=JBAlNUQdWysKIGJJWaZibH8IpkIXP_kUEVxoC_bLbhg,13944
32
+ kmdr/module/downloader/misc.py,sha256=dS4GDHYxzKcqdhFNVJQG-4bS4IXMDnDrwi6eSWubl20,1892
33
+ kmdr/module/lister/BookUrlLister.py,sha256=R1sak_GQG8HKfdSzHF-Dy1iby6TgFlxKJLf2dwBr0Bw,550
34
+ kmdr/module/lister/FollowedBookLister.py,sha256=5r44sW_ypnRM5nBBd-4AL_WcNYCRklMewvMj1O2xCGc,2880
35
+ kmdr/module/lister/__init__.py,sha256=VVRMRGXASdIagzlmOuhy9gKjJzf8Rj3000BPzeWpHHE,91
36
+ kmdr/module/lister/utils.py,sha256=phAdtGI3F5kT6x4JFOiN39Ei_yY5JdwQG-tfRZ6MVvs,3595
37
+ kmdr/module/picker/ArgsFilterPicker.py,sha256=wBCDa6KsSSOLLQk8D4ftZ9ZnwPLiHIirxf6gGBq3I08,1782
38
+ kmdr/module/picker/DefaultVolPicker.py,sha256=qScmRtwpe1u3fY7HMIo15YXtwB4LYAIJaTu_EkrAJbs,1780
39
+ kmdr/module/picker/__init__.py,sha256=qYELEkv0nFT8DadItB6fdUTED2CHrC43wuPKO7QrCCQ,93
40
+ kmdr/module/picker/utils.py,sha256=baJzpQSSSBdTPZ2scrEeNOssGYOL0nNwxN1gTsOAqnc,1604
41
+ kmoe_manga_downloader-1.2.3b0.dist-info/licenses/LICENSE,sha256=bKQlsXu8mAYKRZyoZKOEqMcCc8YjT5Q3Hgr21e0yU4E,1068
42
+ kmoe_manga_downloader-1.2.3b0.dist-info/METADATA,sha256=9Z42LEHwL9wZVCRiUbR7FX2BsO1jBmu5dpXeIFRtN_c,9695
43
+ kmoe_manga_downloader-1.2.3b0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ kmoe_manga_downloader-1.2.3b0.dist-info/entry_points.txt,sha256=DGMytQAhx4uNuKQL7BPkiWESHLXkH-2KSEqwHdygNPA,47
45
+ kmoe_manga_downloader-1.2.3b0.dist-info/top_level.txt,sha256=e0qxOgWp0tl3GLpmXGjZv3--q_TLoJ7GztM48Ov27wM,5
46
+ kmoe_manga_downloader-1.2.3b0.dist-info/RECORD,,
@@ -1,43 +0,0 @@
1
- kmdr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- kmdr/main.py,sha256=EubhpLIrGHl9XjHXdmhROg3RBKyvHPuPaZPTaSYDsxQ,1355
3
- kmdr/core/__init__.py,sha256=mTBPcw-mEKfHPV1bgwDx-JoO2G6dQ9QcOnQiEMmi7DE,342
4
- kmdr/core/bases.py,sha256=muUjcuEm-n51YTHiU7LTwfQm_AwkcNJ5TzIdPZNpK0o,3929
5
- kmdr/core/constants.py,sha256=_jY56Tpsg7U7Xw4Ht4Q46W4fNu8kyNCEicoTjGgKVUI,1802
6
- kmdr/core/context.py,sha256=cy4_Iar-dpsWQiLGIJaitNEVw6uwRkzdvF6iYDjLWl0,930
7
- kmdr/core/defaults.py,sha256=6A3nECLdbb3zVngdFKn_U1GrtcJRMor8Ju4XebCutU8,8459
8
- kmdr/core/error.py,sha256=1UR16-5E-51hV8Rtg1Ua0Ro7jGfmLpFsTBFkoD6hmto,884
9
- kmdr/core/registry.py,sha256=KYjvww5WRuUg5SeIeWZb96803-kgrTIKYvFTj_7bPfo,5560
10
- kmdr/core/session.py,sha256=lFH4lnriwqvw-tlB3MEgRedX7h_LdtaR21Rfo-HAhRs,498
11
- kmdr/core/structure.py,sha256=EQG1-8kHQ22Not2S7Q_jGxE0nb9XUbLh3JcNTb_nkvk,1199
12
- kmdr/core/utils.py,sha256=MbJQM5943IEVr4J4j7zdPKuepKh3iJmI5fy6QYnIVr8,2424
13
- kmdr/module/__init__.py,sha256=hY6lMBpxSn93t7OKnEbXUwTok2XqQoQbuyq1HLeC4Kc,125
14
- kmdr/module/authenticator/CookieAuthenticator.py,sha256=ur_MtaeNmcrLQYpFplTvVGyd1EocR-_jocPj6K5z1uY,1353
15
- kmdr/module/authenticator/LoginAuthenticator.py,sha256=8xtCIwR_GkHniJc2JMYwRYLdL4KfQ8d5phEz_NfzMpU,2529
16
- kmdr/module/authenticator/__init__.py,sha256=iSWhq-suAM1BrABRuTWiCKvZ1fkmf5HQ2GmbZ341GK0,103
17
- kmdr/module/authenticator/utils.py,sha256=7_oHc-9Ab48T-cTNMWEke6_OI0Z3rg-eb5LyzRzCE9M,3179
18
- kmdr/module/configurer/BaseUrlUpdator.py,sha256=cTOadDJI38rIVsCb1UvOqODY_UnxUY-1TzR-tMwKcME,501
19
- kmdr/module/configurer/ConfigClearer.py,sha256=2Ra-EV4fuDilWwoUS8NBSL1__-50D0YD0P6kHurH62c,480
20
- kmdr/module/configurer/ConfigUnsetter.py,sha256=WVCETKg80vGCDEoaN5XWG7MYDJo-eGit-n53rBk46ZE,597
21
- kmdr/module/configurer/OptionLister.py,sha256=wHcx8XGjjUL6Nj4K909aqikVK06UE7CXPo6XkZc3q3c,1466
22
- kmdr/module/configurer/OptionSetter.py,sha256=g3Nid0cG0VFhEguAcHwsOpfOluf-8lKpwZLemrUfP-A,893
23
- kmdr/module/configurer/__init__.py,sha256=nSWGwUCcwOkvg28zxRd0S3b4jeag_W0IhzP74xq0ewE,204
24
- kmdr/module/configurer/option_validate.py,sha256=Mn91UmpTI8zVV9Em8rL2mjNV21h0iYlzT8xoWDLEI68,3387
25
- kmdr/module/downloader/DirectDownloader.py,sha256=IedLaw6cGH-7Rwsvvs_27kqCDbzfHnNT5GP6ERHEo9o,1370
26
- kmdr/module/downloader/ReferViaDownloader.py,sha256=oM9S8BURmMQL3KPSkb0XJw-SvSIzCiJsWse4XIahKCY,1887
27
- kmdr/module/downloader/__init__.py,sha256=PajPBQ4ZJwz_6Ok6k-HV7-YSibxAW5m5voYyP-U-_f4,97
28
- kmdr/module/downloader/download_utils.py,sha256=CXpsMujgAxCQbokJ9RiQdKFozr_AgbhkJ9n-V_JWr4U,11882
29
- kmdr/module/downloader/misc.py,sha256=-gKmqqENQ2ukTVou9cDan0K2MDfwX43W8LyTw3o-oVM,1816
30
- kmdr/module/lister/BookUrlLister.py,sha256=R1sak_GQG8HKfdSzHF-Dy1iby6TgFlxKJLf2dwBr0Bw,550
31
- kmdr/module/lister/FollowedBookLister.py,sha256=WeMm7c81hbLr5TkrgQiBx8pQ1odOxWXn_xvQ9LxYZYs,2933
32
- kmdr/module/lister/__init__.py,sha256=VVRMRGXASdIagzlmOuhy9gKjJzf8Rj3000BPzeWpHHE,91
33
- kmdr/module/lister/utils.py,sha256=iUA0zu8JzKUouT3rwM_fr8knlHSPqApqeGSYOFI2D7I,3575
34
- kmdr/module/picker/ArgsFilterPicker.py,sha256=wBCDa6KsSSOLLQk8D4ftZ9ZnwPLiHIirxf6gGBq3I08,1782
35
- kmdr/module/picker/DefaultVolPicker.py,sha256=XD5mo48bwpa6T69AIxWJVvrX1daPsxyG8AFsJ4RLTBc,1760
36
- kmdr/module/picker/__init__.py,sha256=qYELEkv0nFT8DadItB6fdUTED2CHrC43wuPKO7QrCCQ,93
37
- kmdr/module/picker/utils.py,sha256=lpxM7q9BJeupFQy8glBrHu1o4E38dk7iLexzKytAE6g,1222
38
- kmoe_manga_downloader-1.2.1.dist-info/licenses/LICENSE,sha256=bKQlsXu8mAYKRZyoZKOEqMcCc8YjT5Q3Hgr21e0yU4E,1068
39
- kmoe_manga_downloader-1.2.1.dist-info/METADATA,sha256=0YGgNLfT2bFWscpO-N3QWsDbu19hMEYcXlb5yFbo3Wc,9693
40
- kmoe_manga_downloader-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- kmoe_manga_downloader-1.2.1.dist-info/entry_points.txt,sha256=DGMytQAhx4uNuKQL7BPkiWESHLXkH-2KSEqwHdygNPA,47
42
- kmoe_manga_downloader-1.2.1.dist-info/top_level.txt,sha256=e0qxOgWp0tl3GLpmXGjZv3--q_TLoJ7GztM48Ov27wM,5
43
- kmoe_manga_downloader-1.2.1.dist-info/RECORD,,