Skip to content

Dev#15

Closed
qwasd7680 wants to merge 5 commits intomainfrom
dev
Closed

Dev#15
qwasd7680 wants to merge 5 commits intomainfrom
dev

Conversation

@qwasd7680
Copy link
Copy Markdown
Owner

No description provided.

@qwasd7680 qwasd7680 requested a review from Copilot October 30, 2025 20:02
Comment thread main.py
Comment on lines +60 to +61
except Exception:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception handling in the _send_and_close method is using a bare except statement, which can suppress important errors and make debugging difficult. It's recommended to log the exception or handle specific exceptions to ensure that important issues are not missed.

Recommended Change:

try:
    await websocket.close()
except Exception as e:
    print(f"Error closing WebSocket for client {client_id}: {str(e)}")

Comment thread main.py
Comment on lines 155 to +166
if zip_file_path.exists():
# 启动延迟删除线程 (0.5 * 60 * 60 秒 = 30 分钟)
threading.Thread(target=delayed_delete,
args=(zip_file_path, int(0.5 * 60 * 60)),
daemon=True).start()

# 任务成功,使用 asyncio.run() 在当前线程中运行异步通知任务
asyncio.run(manager.send_personal_message(client_id, {
message = {
"status": "download_ready",
"file_name": file_title,
"message": f"文件 '{file_title}' 已完成处理,可以下载。"
}))

}
else:
# 任务失败,发送 WebSocket 错误通知
asyncio.run(manager.send_personal_message(client_id, {
message = {
"status": "error",
"file_name": file_title,
"message": f"文件 '{file_title}' 未找到或处理失败。"
}))

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for checking the existence of the zip file and preparing the message to send to the client does not account for the scenario where the file does not exist but the message indicates it's ready. This could mislead the client into thinking a file is ready for download when it isn't.

Recommended Change:

if zip_file_path.exists():
    message = {
        "status": "download_ready",
        "file_name": file_title,
        "message": f"文件 '{file_title}' 已完成处理,可以下载。"
    }
else:
    message = {
        "status": "error",
        "file_name": file_title,
        "message": f"文件 '{file_title}' 未找到或处理失败。"
    }

Comment thread tests/test_api.py
Comment on lines +19 to +79
response = client.get("/v1/{0}".format(nowtimestamp))
timedelta = int(time.time() * 1000) - nowtimestamp
ms = int(timedelta)
assert response.status_code == 200
assert response.json().get("status") == "ok"
assert response.json().get("app") == "jmcomic_server_api"
assert int(response.json().get("latency")) <= ms
assert int(response.json().get("latency")) > 0


def test_search_album():
client = TestClient(app)
tag = "全彩"
num = 1
response = client.get("/v1/search/{0}/{1}".format(tag, num))
assert response.status_code == 200
assert isinstance(response.json(), list)
assert len(response.json()) > 0
first_album = response.json()[0]
assert "album_id" in first_album
assert "title" in first_album


def test_get_cover_and_info():
client = TestClient(app)
aid = 1225432
response = client.get("/v1/info/{0}".format(aid))
assert response.status_code == 200
info_json = response.json()
assert info_json.get("status") == "success"
assert "全彩" in info_json.get("tag", [])
assert int(info_json.get("view_count")) > 0
assert int(info_json.get("like_count")) > 0
assert info_json.get("page_count") == "0"
response = client.get("/v1/get/cover/{0}".format(aid))
assert response.status_code == 200
assert response.headers["content-type"] == "image/jpeg"
file_path = FILE_PATH / f"cover-{aid}.jpg"
assert file_path.exists()
file_path.unlink()
assert file_path.exists() == False


def test_download_album():
client = TestClient(app)
aid = 1225432
client_id = "1145141919810"
with client.websocket_connect(f"/ws/notifications/{client_id}") as websocket:
response = client.post(f"/v1/download/album/{aid}", json={"client_id": client_id})
assert response.status_code == 202
assert response.json() == {
"status": "processing",
"message": "下载任务已在后台启动,请通过 WebSocket 监听 'download_ready' 通知。"
}
data = websocket.receive_json()
assert data == {
"status": "download_ready",
"file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
"message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
}
client.get("/v1/download/album/{0}".format(aid))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of .format() for URL construction is outdated and less readable compared to f-strings, which are available in Python 3.6 and later. Consider refactoring these to use f-strings for better readability and maintainability.

Recommended Change:

response = client.get(f"/v1/{nowtimestamp}")
# Similarly for other lines

Comment thread tests/test_api.py
Comment on lines +57 to +85
assert file_path.exists()
file_path.unlink()
assert file_path.exists() == False


def test_download_album():
client = TestClient(app)
aid = 1225432
client_id = "1145141919810"
with client.websocket_connect(f"/ws/notifications/{client_id}") as websocket:
response = client.post(f"/v1/download/album/{aid}", json={"client_id": client_id})
assert response.status_code == 202
assert response.json() == {
"status": "processing",
"message": "下载任务已在后台启动,请通过 WebSocket 监听 'download_ready' 通知。"
}
data = websocket.receive_json()
assert data == {
"status": "download_ready",
"file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
"message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
}
client.get("/v1/download/album/{0}".format(aid))
file_title = "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分"
zip_file_name = f"{file_title}.zip"
zip_file_path = FILE_PATH / zip_file_name
assert zip_file_path.exists() == True
zip_file_path.unlink()
assert zip_file_path.exists() == False
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of checking if a file exists and then deleting it can lead to race conditions, especially in a multi-threaded or multi-process environment. This could make the tests flaky. Consider using a try-except block to handle potential FileNotFoundError which would be more robust.

Recommended Change:

try:
    file_path.unlink()
except FileNotFoundError:
    pass

Comment thread tests/test_jmcomic_lib.py
Comment on lines 10 to 22
def test_get_comic_info():
testClient = jmcomic.JmOption.default().new_jm_client()
page = testClient.search_site(search_query="1225432")
album: jmcomic.JmAlbumDetail = page.single_album
assert album.title == "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分"
assert album.tags == ["全彩","贫乳","调教","中文"]
assert album.tags == ["全彩", "贫乳", "调教", "中文"]
assert album.views is not None
assert album.likes is not None


def test_rank_comic():
client = jmcomic.JmOption.default().new_jm_client()
page1: jmcomic.JmCategoryPage = client.month_ranking(1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Test Dependence on External State

The tests test_get_comic_info and test_rank_comic are heavily dependent on the external state of a comic database. This dependence can lead to flaky tests if the external data changes.

Recommendation:

  • Mock the responses from jmcomic.JmOption.default().new_jm_client() to ensure that the tests are not affected by changes in external data. This will make the tests more reliable and predictable.

Comment thread tests/test_jmcomic_lib.py
Comment on lines 6 to 7
FILE_PATH = Path(f"{current_dir}/temp")
os.makedirs(FILE_PATH, exist_ok=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Lack of Cleanup for Temporary Directory

The directory created for temporary files in the test setup is not cleaned up after the tests, which can lead to residual data accumulation.

Recommendation:

  • Implement a teardown mechanism or use a Python context manager to ensure that the directory is removed after the tests are completed. This will help in maintaining a clean test environment and prevent issues related to leftover data.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request refactors the WebSocket implementation for album download notifications and adds comprehensive API test coverage. The main changes focus on improving thread-safe communication between background tasks and WebSocket clients, along with code formatting improvements.

Key changes:

  • Refactored WebSocket connection management to use asyncio.run_coroutine_threadsafe() for thread-safe message delivery from background tasks
  • Added comprehensive API tests covering health checks, search, info retrieval, and full download workflows with WebSocket integration
  • Updated formatting in existing tests and removed --no-cache-dir flag from Dockerfile

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
main.py Refactored ConnectionManager to track the main event loop and use thread-safe coroutine execution; removed client_id validation and WebSocket receive loop; removed delayed file deletion after download
tests/test_api.py Added comprehensive API tests including WebSocket-based download workflow testing
tests/test_jmcomic_lib.py Minor formatting improvements (spacing in list literals and blank lines)
requirements.txt Added httpx dependency for test client
README.md Removed --no-cache-dir flag from pip install command

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main.py
Comment on lines 97 to 98


Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WebSocket endpoint no longer maintains an active connection or handles disconnections. After connect() is called, the function immediately returns, causing the WebSocket to close. This breaks the expected behavior where clients should maintain a persistent connection to receive notifications. The removed try-except block with the receive loop was necessary to keep the connection alive until the task completes and notification is sent.

Suggested change
try:
while True:
# Keep the connection alive by waiting for messages from the client
await websocket.receive_text()
except WebSocketDisconnect:
pass
finally:
await manager.disconnect(client_id)

Copilot uses AI. Check for mistakes.
Comment thread main.py
Comment on lines 155 to +160
if zip_file_path.exists():
# 启动延迟删除线程 (0.5 * 60 * 60 秒 = 30 分钟)
threading.Thread(target=delayed_delete,
args=(zip_file_path, int(0.5 * 60 * 60)),
daemon=True).start()

# 任务成功,使用 asyncio.run() 在当前线程中运行异步通知任务
asyncio.run(manager.send_personal_message(client_id, {
message = {
"status": "download_ready",
"file_name": file_title,
"message": f"文件 '{file_title}' 已完成处理,可以下载。"
}))

}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delayed deletion thread that was previously started for downloaded zip files has been removed. This means downloaded zip files will never be cleaned up, leading to unbounded disk space usage over time. The previous implementation started a 30-minute delayed deletion thread, which should be preserved or replaced with an alternative cleanup mechanism.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_api.py
file_path = FILE_PATH / f"cover-{aid}.jpg"
assert file_path.exists()
file_path.unlink()
assert file_path.exists() == False
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comparison to boolean literal. Should use assert not file_path.exists() instead of assert file_path.exists() == False for better readability and following Python best practices.

Suggested change
assert file_path.exists() == False
assert not file_path.exists()

Copilot uses AI. Check for mistakes.
Comment thread tests/test_api.py
file_title = "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分"
zip_file_name = f"{file_title}.zip"
zip_file_path = FILE_PATH / zip_file_name
assert zip_file_path.exists() == True
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comparison to boolean literal. Should use assert zip_file_path.exists() instead of assert zip_file_path.exists() == True for better readability and following Python best practices.

Suggested change
assert zip_file_path.exists() == True
assert zip_file_path.exists()

Copilot uses AI. Check for mistakes.
Comment thread main.py
Comment on lines +60 to +61
except Exception:
pass
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
pass
except Exception as e:
print(f"[WebSocket Error] 关闭客户端 {client_id} 的连接时发生异常: {e}")

Copilot uses AI. Check for mistakes.
Comment thread tests/test_api.py
Comment on lines +18 to +21
assert response.json().get("status") == "ok"
assert response.json().get("app") == "jmcomic_server_api"
assert int(response.json().get("latency")) <= ms
assert int(response.json().get("latency")) > 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response.json() method is called multiple times within the same test function, which results in parsing the JSON response multiple times. This is inefficient as it unnecessarily repeats the parsing process.

Recommended Change:
Store the result of response.json() in a variable and reuse it:

json_response = response.json()
assert json_response.get("status") == "ok"
assert json_response.get("app") == "jmcomic_server_api"
assert int(json_response.get("latency")) <= ms
assert int(json_response.get("latency")) > 0

Comment thread tests/test_api.py
Comment on lines +61 to +73
with client.websocket_connect(f"/ws/notifications/{client_id}") as websocket:
response = client.post(f"/v1/download/album/{aid}", json={"client_id": client_id})
assert response.status_code == 202
assert response.json() == {
"status": "processing",
"message": "下载任务已在后台启动,请通过 WebSocket 监听 'download_ready' 通知。"
}
data = websocket.receive_json()
assert data == {
"status": "download_ready",
"file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
"message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WebSocket communication assumes successful data reception without handling potential exceptions or errors. This can lead to unhandled exceptions if the WebSocket fails to receive data or receives unexpected data.

Recommended Change:
Implement error handling for WebSocket operations to ensure robustness. Use a try-except block to catch exceptions such as WebSocketDisconnect and handle unexpected data formats:

try:
    data = websocket.receive_json()
    assert data == {
        "status": "download_ready",
        "file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
        "message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
    }
except WebSocketDisconnect:
    # Handle disconnection appropriately
except json.JSONDecodeError:
    # Handle JSON parsing errors

Comment thread tests/test_api.py
Comment on lines +18 to +22
assert response.status_code == 200
assert response.json().get("status") == "ok"
assert response.json().get("app") == "jmcomic_server_api"
assert int(response.json().get("latency")) <= ms
assert int(response.json().get("latency")) > 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response.json() method is called multiple times within the same test function, which results in parsing the JSON response multiple times. This is inefficient as it unnecessarily repeats the parsing process.

Recommended Change:
Store the result of response.json() in a variable and reuse it:

json_response = response.json()
assert json_response.get("status") == "ok"
assert json_response.get("app") == "jmcomic_server_api"
assert int(json_response.get("latency")) \<= ms
assert int(json_response.get("latency")) \> 0

Comment thread tests/test_api.py
Comment on lines +62 to +74
with client.websocket_connect(f"/ws/notifications/{client_id}") as websocket:
response = client.post(f"/v1/download/album/{aid}", json={"client_id": client_id})
assert response.status_code == 202
assert response.json() == {
"status": "processing",
"message": "下载任务已在后台启动,请通过 WebSocket 监听 'download_ready' 通知。"
}
data = websocket.receive_json()
assert data == {
"status": "download_ready",
"file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
"message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WebSocket communication assumes successful data reception without handling potential exceptions or errors. This can lead to unhandled exceptions if the WebSocket fails to receive data or receives unexpected data.

Recommended Change:
Implement error handling for WebSocket operations to ensure robustness. Use a try-except block to catch exceptions such as WebSocketDisconnect and handle unexpected data formats:

try:
    data = websocket.receive_json()
    assert data == {
        "status": "download_ready",
        "file_name": "[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分",
        "message": f"文件 '[酸菜鱼ゅ°]ヒルチャールに败北した胡桃 表情、台词差分' 已完成处理,可以下载。"
    }
except WebSocketDisconnect:
    # Handle disconnection appropriately
except json.JSONDecodeError:
    # Handle JSON parsing errors

@qwasd7680 qwasd7680 closed this Oct 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants