')
@@ -654,6 +798,54 @@ def parse_api_to_favorite_page(cls, data: AdvancedDict) -> JmFavoritePage:
return JmFavoritePage(content, folder_list, total)
+ @classmethod
+ def parse_api_to_album_comment_page(cls, data: AdvancedDict, page=1) -> JmAlbumCommentPage:
+ def parse_comment(item):
+ item_data = getattr(item, 'src_dict', item) or {}
+ parser = cls.HtmlTextParser()
+ parser.feed(item_data.get('content') or '')
+ parser.close()
+ comment = JmAlbumComment(item)
+ comment.content = ''.join(parser.parts).strip()
+ comment.replies = [
+ parse_comment(reply.raw_data)
+ for reply in comment.replies
+ ]
+ return comment
+
+ content = [
+ parse_comment(item)
+ for item in data.get('list', []) or []
+ ]
+
+ total = data.get('total')
+ if total is not None:
+ try:
+ total = int(total)
+ except (TypeError, ValueError):
+ total = None
+
+ return JmAlbumCommentPage(
+ content=content,
+ total=total,
+ raw_data=data,
+ )
+
+ @classmethod
+ def parse_html_to_album_comment_page(cls, data: AdvancedDict, page=1) -> JmAlbumCommentPage:
+ raw_html = data.code or ''
+ parser = cls.CommentParser()
+ parser.feed(raw_html)
+ parser.close()
+ content = [JmAlbumComment(item) for item in parser.comments]
+
+ return JmAlbumCommentPage(
+ content=content,
+ total=None,
+ raw_html=raw_html,
+ raw_data=data,
+ )
+
@classmethod
def adapt_content(cls, content):
def adapt_item(item: AdvancedDict):
diff --git a/tests/test_jmcomic/test_jm_api.py b/tests/test_jmcomic/test_jm_api.py
index d9af3553a..bf46f99cc 100644
--- a/tests/test_jmcomic/test_jm_api.py
+++ b/tests/test_jmcomic/test_jm_api.py
@@ -1,8 +1,44 @@
+import asyncio
+import inspect
+from unittest.mock import AsyncMock, patch
+
from test_jmcomic import *
class Test_Api(JmTestConfigurable):
+ def test_callback_is_not_public_download_api(self):
+ for download_api in (
+ download_album,
+ download_photo,
+ download_album_async,
+ download_photo_async,
+ ):
+ parameters = inspect.signature(download_api).parameters
+ self.assertNotIn('callback', parameters)
+ self.assertEqual(
+ parameters['check_exception'].kind,
+ inspect.Parameter.KEYWORD_ONLY,
+ )
+
+ def test_multi_id_shortcuts_do_not_forward_check_exception(self):
+ for download_api in (download_album, download_photo):
+ expected = BatchResult()
+ with patch('jmcomic.api.download_batch', return_value=expected) as batch:
+ actual = download_api(['123', '456'], check_exception=False)
+
+ self.assertIs(actual, expected)
+ self.assertNotIn('check_exception', batch.call_args.kwargs)
+
+ for download_api in (download_album_async, download_photo_async):
+ expected = BatchResult()
+ batch = AsyncMock(return_value=expected)
+ with patch('jmcomic.api.download_batch_async', new=batch):
+ actual = asyncio.run(download_api(['123', '456'], check_exception=False))
+
+ self.assertIs(actual, expected)
+ self.assertNotIn('check_exception', batch.call_args.kwargs)
+
def test_download_photo_by_id(self):
"""
测试jmcomic模块的api的使用
diff --git a/tests/test_jmcomic/test_jm_async_api.py b/tests/test_jmcomic/test_jm_async_api.py
index 47aa774a1..995b3663d 100644
--- a/tests/test_jmcomic/test_jm_async_api.py
+++ b/tests/test_jmcomic/test_jm_async_api.py
@@ -6,7 +6,6 @@
"""
import asyncio
-import threading
from test_jmcomic import *
from jmcomic import (
download_album_async, download_photo_async, download_batch_async,
@@ -22,13 +21,6 @@ class Test_Async_Api(JmAsyncTestConfigurable):
def test_async_download_photo_by_id(self):
"""测试 download_photo_async:验证返回值与同步版本保持一致"""
photo_id = '438516'
- callback_result = {}
- caller_thread = threading.get_ident()
-
- def callback(photo, downloader):
- callback_result['photo'] = photo
- callback_result['downloader'] = downloader
- callback_result['thread'] = threading.get_ident()
# sync
sync_photo, sync_dler = download_photo(photo_id, self.option)
@@ -36,26 +28,16 @@ def callback(photo, downloader):
async_photo, async_dler = asyncio.run(download_photo_async(
photo_id,
self.option,
- callback=callback,
))
self.assertIsInstance(async_dler, JmAsyncDownloader, 'downloader 必须是异步版本')
self.assertIsInstance(async_photo, JmPhotoDetail, '返回值必须包含 photo')
self.assert_sync_async_equal(sync_photo.photo_id, async_photo.photo_id, 'photo.photo_id')
- self.assertIs(callback_result['photo'], async_photo)
- self.assertIs(callback_result['downloader'], async_dler)
- self.assertNotEqual(callback_result['thread'], caller_thread, '同步 callback 不应阻塞事件循环线程')
self.assertIsNone(async_dler.client, '顶层异步下载结束后 downloader client 应已关闭')
def test_async_download_album_by_id(self):
"""测试 download_album_async:验证返回值与同步版本保持一致"""
album_id = '438516'
- callback_result = {}
-
- async def callback(album, downloader):
- await asyncio.sleep(0)
- callback_result['album'] = album
- callback_result['downloader'] = downloader
# sync
sync_album, sync_dler = download_album(album_id, self.option)
@@ -63,14 +45,11 @@ async def callback(album, downloader):
async_album, async_dler = asyncio.run(download_album_async(
album_id,
self.option,
- callback=callback,
))
self.assertIsInstance(async_dler, JmAsyncDownloader, 'downloader 必须是异步版本')
self.assertIsInstance(async_album, JmAlbumDetail, '返回值必须包含 album')
self.assert_album_equal(sync_album, async_album)
- self.assertIs(callback_result['album'], async_album)
- self.assertIs(callback_result['downloader'], async_dler)
self.assertIsNone(async_dler.client, '顶层异步下载结束后 downloader client 应已关闭')
def test_async_batch(self):
diff --git a/tests/test_jmcomic/test_jm_async_client.py b/tests/test_jmcomic/test_jm_async_client.py
index a7682ab26..15437b6f1 100644
--- a/tests/test_jmcomic/test_jm_async_client.py
+++ b/tests/test_jmcomic/test_jm_async_client.py
@@ -71,6 +71,33 @@ def test_async_comment_count(self):
)
self.assertGreater(async_album.comment_count, 0, 'comment_count 应 > 0')
+ def test_async_album_pagination(self):
+ """测试异步评论分页与生成器。"""
+ album_id = '302820'
+
+ async def run():
+ comment_gen = self.async_client.album_pagination_gen(album_id)
+ page_1 = await comment_gen.__anext__()
+ page_2 = await comment_gen.__anext__()
+ await comment_gen.aclose()
+
+ self.assertTrue(list(page_1))
+ self.assertTrue(list(page_2))
+ self.assertEqual(page_1.total, page_2.total)
+ self.assertIsInstance(page_1.raw_data, AdvancedDict)
+ self.assertGreaterEqual(page_1.comment_count, len(page_1))
+
+ comment = page_1[0]
+ self.assertIsInstance(comment, JmAlbumComment)
+ self.assertIsInstance(comment.raw_data, AdvancedDict)
+ self.assertIsInstance(comment.is_spoiler, bool)
+ self.assertTrue(all(
+ isinstance(reply, JmAlbumComment)
+ for reply in comment.replies
+ ))
+
+ self.run_async(run())
+
def test_async_get_detail(self):
"""对标 test_get_detail:album + photo 联合 diff"""
album_id = 400222
diff --git a/tests/test_jmcomic/test_jm_cli.py b/tests/test_jmcomic/test_jm_cli.py
index 3db1b2370..bdbd913de 100644
--- a/tests/test_jmcomic/test_jm_cli.py
+++ b/tests/test_jmcomic/test_jm_cli.py
@@ -2,7 +2,7 @@
from io import StringIO
from unittest.mock import patch
-from jmcomic.cl import JmcomicUI, JmViewUI
+from jmcomic.cli import JmcomicUI, JmViewUI
class Test_Cli(JmTestConfigurable):
@@ -10,6 +10,12 @@ class Test_Cli(JmTestConfigurable):
album_id = '350234'
+ def test_cl_deprecated(self):
+ with self.assertWarnsRegex(DeprecationWarning, r'removed in version 2\.7\.4'):
+ from jmcomic.cl import JmcomicUI as DeprecatedJmcomicUI
+
+ self.assertIs(DeprecatedJmcomicUI, JmcomicUI)
+
# ========== jmcomic 命令测试 ==========
def test_jmcomic_parse_album_id(self):
diff --git a/tests/test_jmcomic/test_jm_client.py b/tests/test_jmcomic/test_jm_client.py
index 7abc93bea..70b2a6563 100644
--- a/tests/test_jmcomic/test_jm_client.py
+++ b/tests/test_jmcomic/test_jm_client.py
@@ -222,6 +222,140 @@ def test_comment_count(self):
aid,
)
+ def test_album_pagination(self):
+ album_id = '302820'
+ api_client = self.option.new_jm_client(impl='api')
+ html_client = self.option.new_jm_client(impl='html')
+
+ api_gen = api_client.album_pagination_gen(album_id)
+ api_page = next(api_gen)
+ api_page_2 = next(api_gen)
+ api_gen.close()
+
+ html_page = None
+ for domain in html_client.get_html_domain_all():
+ html_client.set_domain_list([domain])
+ try:
+ html_page = html_client.album_pagination(album_id, page=1)
+ break
+ except Exception:
+ continue
+
+ self.assertIsNotNone(html_page, '所有网页域名均无法获取本子评论')
+ api_comments = list(api_page)
+ html_comments = list(html_page)
+
+ self.assertTrue(api_comments)
+ self.assertTrue(list(api_page_2))
+ self.assertTrue(html_comments)
+ self.assertIsInstance(api_page.raw_data, AdvancedDict)
+ self.assertIsInstance(html_page.raw_data, AdvancedDict)
+ self.assertGreaterEqual(api_page.comment_count, len(api_page))
+ self.assertGreaterEqual(html_page.comment_count, len(html_page))
+ self.assertGreater(api_page.total, 0)
+ self.assertGreater(html_page.total, 0)
+ self.assertEqual(api_page.total, api_page_2.total)
+
+ html_gen_without_total = html_client.album_pagination_gen(
+ album_id,
+ page=1,
+ need_total=False,
+ )
+ html_page_without_total = next(html_gen_without_total)
+ html_page_2_without_total = next(html_gen_without_total)
+ html_gen_without_total.close()
+
+ self.assertTrue(list(html_page_2_without_total))
+ self.assertIsNone(html_page_without_total.total)
+ self.assertIsNone(html_page_without_total.page_count)
+ self.assertIsNone(html_page_2_without_total.total)
+ self.assertIsNone(html_page_2_without_total.page_count)
+
+ for comment in (api_comments[0], html_comments[0]):
+ self.assertIsInstance(comment.raw_data, AdvancedDict)
+ self.assertTrue(comment.comment_id)
+ self.assertEqual(str(comment.album_id), album_id)
+ self.assertIsInstance(comment.content, str)
+ self.assertIsInstance(comment.is_spoiler, bool)
+
+ api_comments_by_cid = {
+ str(comment.comment_id): comment
+ for comment in api_comments
+ }
+ html_comments_by_cid = {
+ str(comment.comment_id): comment
+ for comment in html_comments
+ }
+ common_cids = api_comments_by_cid.keys() & html_comments_by_cid.keys()
+ self.assertTrue(common_cids)
+
+ checked_reply = False
+ api_pages = {1: api_page, 2: api_page_2}
+ for reply_page_number in range(1, 6):
+ candidate_api_page = api_pages.get(reply_page_number)
+ if candidate_api_page is None:
+ candidate_api_page = api_client.album_pagination(
+ album_id,
+ page=reply_page_number,
+ )
+ candidate_html_page = (
+ html_page_without_total
+ if reply_page_number == 1
+ else html_client.album_pagination(
+ album_id,
+ page=reply_page_number,
+ need_total=False,
+ )
+ )
+ html_reply_comments = {
+ str(comment.comment_id): comment
+ for comment in candidate_html_page
+ }
+ for api_comment in candidate_api_page:
+ api_replies = api_comment.replies
+ html_comment = html_reply_comments.get(str(api_comment.comment_id))
+ if not api_replies or html_comment is None:
+ continue
+
+ html_replies = html_comment.replies
+ if not html_replies:
+ continue
+
+ api_replies_by_cid = {
+ str(reply.comment_id): reply
+ for reply in api_replies
+ }
+ html_replies_by_cid = {
+ str(reply.comment_id): reply
+ for reply in html_replies
+ }
+ common_reply_cids = api_replies_by_cid.keys() & html_replies_by_cid.keys()
+ if not common_reply_cids:
+ continue
+
+ reply_cid = next(iter(common_reply_cids))
+ api_reply = api_replies_by_cid[reply_cid]
+ html_reply = html_replies_by_cid[reply_cid]
+ self.assertEqual(str(html_reply.comment_id), reply_cid)
+ self.assertEqual(str(api_reply.comment_id), reply_cid)
+ self.assertIsInstance(api_reply.raw_data, AdvancedDict)
+ self.assertIsInstance(html_reply.raw_data, AdvancedDict)
+ self.assertIsInstance(api_reply.content, str)
+ self.assertFalse(api_reply.content.startswith('