Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,22 @@ public void toggleAlbumFavorite(String albumId, String folderId) {
HttpUrl url = newHttpUrlBuilder()
.addPathSegment("ajax")
.addPathSegment("favorite_album")
.addQueryParameter("album_id", albumId)
.addQueryParameter("fid", folderId == null ? "0" : folderId)
.build();

JmHtmlResponse jmHtmlResponse = executeGetRequest(url);
HttpUrl urlDel = newHttpUrlBuilder()
.addPathSegment("ajax")
.addPathSegment("delete_favorite_album")
.build();

FormBody.Builder formBuilder = new FormBody.Builder()
.add("album_id", albumId)
.add("fid", folderId == null ? "0" : folderId);

FormBody.Builder formBuilderDel = new FormBody.Builder()
.add("album_id", albumId);

JmHtmlResponse jmHtmlResponse = executePostRequest(url, formBuilder.build());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  'getCachedJmFavoritePage|cacheJmFavoritePage|getCachedJmAlbum|cacheJmAlbum|invalidate|evict|clear.*Cache' \
  jmcomic-core/src/main/java

Repository: JUKOMU/JMComic-Api-Java

Length of output: 29394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 查看目标文件的关键方法概要,然后定位收藏切换和收藏夹相关实现。
ast-grep outline jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java --view expanded | sed -n '1,260p'

printf '\n== target lines 330-375 ==\n'
sed -n '330,375p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n -v

printf '\n== target lines 495-530 ==\n'
sed -n '495,530p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n -v

printf '\n== CachePool cache removal methods ==\n'
rg -n -C 4 'public|void|put|get|remove|keys|size|currentSize|freqMap|node' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/cache/CachePool.java | sed -n '1,260p'

Repository: JUKOMU/JMComic-Api-Java

Length of output: 13306


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== JmHtmlClient toggleAlbumFavorite full method =='
sed -n '333,500p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n -v

printf '%s\n' '== AbstractJmClient cache operations summary =='
sed -n '960,1030p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java | cat -n -v

printf '%s\n' '== JmHtmlClient AbstractJmClient remove inheritance/use =="
rg -n 'class JmHtmlClient|extends AbstractJmClient|protected void cache|protected.*getCached' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java

printf '%s\n' '== inspect methods called around toggleAlbumFavorite =="
sed -n '295,315p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n -v

Repository: JUKOMU/JMComic-Api-Java

Length of output: 12970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== JmHtmlClient inheritance =='
sed -n '1,55p;88,95p;183,210p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n -v

printf '%s\n' '== AbstractJmClient cache methods and removal helpers summary =='
python3 - <<'PY'
from pathlib import Path
p = Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java')
lines = p.read_text(errors='replace').splitlines()
for i,l in enumerate(lines, 1):
    if 'protected' in l or 'public' in l or 'remove' in l or 'cachePool' in l:
        if any(s in l for s in ['getCachedJmAlbum', 'getCachedJmFavoritePage', 'cacheJmAlbum', 'cacheJmFavoritePage', 'CacheKey', 'remove']):
            start=max(1,i-2); end=min(len(lines),i+6)
            print(f'-- lines {start}-{end} --')
            for n in range(start,end+1):
                print(f'{n:5d}: {lines[n-1]}')
PY

printf '%s\n' '== CachePool keys =="
python3 - <<'PY'
from pathlib import Path
p = Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/cache/CachePool.java')
print(p.read_text(errors='replace'))
PY

printf '%s\n' '== CacheKey of methods ='
rg -n -C 3 'class CacheKey|record CacheKey|public static .*of|static .*of|CacheKey\.of' .

Repository: JUKOMU/JMComic-Api-Java

Length of output: 9577


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 使用 Python 读取文件,避免 shell 转义问题。
python3 - <<'PY'
from pathlib import Path

files = {
    'JmHtmlClient': Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java'),
    'AbstractJmClient': Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java'),
    'CacheKey': Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/cache/CacheKey.java'),
    'CachePool': Path('jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/cache/CachePool.java'),
}
for name, p in files.items():
    lines = p.read_text(errors='replace').splitlines()
    print(f'== {name}: {p} ({len(lines)} lines) ==')
    for i,l in enumerate(lines,1):
        if name == 'CacheKey':
            print(f'{i:5d}: {l}')
        elif any(s in l for s in ['toggleAlbumFavorite', 'manageFavoriteFolder', 'getCachedJmAlbum', 'getCachedJmFavoritePage', 'cacheJmAlbum', 'cacheJmFavoritePage', 'CacheKey.of', 'cachePool.remove', 'remove(']):
            start=max(1,i-3); end=min(len(lines),i+6)
            print(f'-- {start}-{end} --')
            for n in range(start,end+1):
                print(f'{n:5d}: {lines[n-1]}')
    print()

# 收集收藏相关操作成功后的缓存调用
for name, p in files.items():
    lines = p.read_text(errors='replace').splitlines()
    print(f'== cache calls after favorite album methods in {name} ==')
    in_method=False
    method_ends_at=0
    cache_calls=[]
    for i,l in enumerate(lines,1):
        if 'public void toggleAlbumFavorite' in l or 'public JmFavoriteFolderResult manageFavoriteFolder' in l:
            in_method=True
        if in_method and l.strip().startswith('}'):
            method_ends_at=i
        if in_method and i>=method_ends_at-100 and i<=method_ends_at:
            if 'cachePool.' in l or 'remove(' in l or 'cacheJm' in l or 'getCachedJm' in l:
                cache_calls.append((i,l.strip()))
    for call in cache_calls:
        print(f'{call[0]}: {call[1]}')
PY

Repository: JUKOMU/JMComic-Api-Java

Length of output: 12108


服务端收藏数据变更后必须失效本地缓存。 采集客户端在收藏切换或收藏夹变更后不会调用 cachePool.remove,后续 getAlbum / getFavorites 仍可能直接返回旧缓存。

  • JmHtmlClient: toggleAlbumFavorite() 成功后应移除缓存的相关 JmAlbumJmFavoritePage
  • JmHtmlClient: manageFavoriteFolder() 成功后应移除受影响的 JmFavoritePage 缓存。
📍 Affects 1 file
  • jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java#L357-L357 (this comment)
  • jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java#L515-L517
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java`
at line 357, 在 JmHtmlClient.java 的 357-357 行对应的 toggleAlbumFavorite() 成功流程中,调用
cachePool.remove 清除受影响的 JmAlbum 和 JmFavoritePage 缓存;在 515-517 行对应的
manageFavoriteFolder() 成功流程中,清除受影响的 JmFavoritePage 缓存,确保后续 getAlbum 和
getFavorites 不返回旧数据。


try {
/*
* 网页端收藏接口返回 JSON,status=1 成功,=0 表示已收藏过。
Expand All @@ -356,6 +367,15 @@ public void toggleAlbumFavorite(String albumId, String folderId) {
status = jsonObject.get("status").getAsInt();
}

if (status == 0) {
// 删除收藏
JmHtmlResponse jmHtmlResponseDel = executePostRequest(urlDel, formBuilderDel.build());
jsonObject = JsonParser.parseString(jmHtmlResponseDel.getHtml()).getAsJsonObject();
if (jsonObject.has("status") && !jsonObject.get("status").isJsonNull()) {
status = jsonObject.get("status").getAsInt();
}
}
Comment on lines +370 to +377

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

仅在响应明确表示已收藏时执行删除。

status 的默认值是 0。如果响应 JSON 缺少 status,当前代码也会调用 delete_favorite_album。这会把协议变更或不完整响应误判为“已收藏”,并删除现有收藏。

先拒绝缺失或为 nullstatus。仅在已成功解析且值确实为 0 时执行删除。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java`
around lines 370 - 377, Update the deletion flow in JmHtmlClient around the
status parsing and delete_favorite_album request so a missing or null response
status does not fall through to deletion. Only invoke the delete request when
the response status is successfully parsed and explicitly equals 0; otherwise
preserve the existing status without deleting.


if (status != 1) {
String message = "";
if (jsonObject.has("msg") && !jsonObject.get("msg").isJsonNull()) {
Expand Down Expand Up @@ -453,9 +473,48 @@ public JmAlbumDownloadInfo getAlbumDownloadInfo(String albumId) {
throw new UnsupportedOperationException("Getting album download info via HTML client is not currently supported. Use JmApiClient instead.");
}

/**
* 未完成
* 移动作品到文件夹只对已经收藏作品的有效
* 删除文件夹只能删除空文件夹
* @param type 操作类型 (add/edit/move/del)
* @param folderId 文件夹ID
* @param folderName 文件夹名称(添加/重命名时需要)
* @param albumId 本子ID(移动时需要)
* @return
*/
@Override
public JmFavoriteFolderResult manageFavoriteFolder(FavoriteFolderType type, String folderId, String folderName, String albumId) {
throw new UnsupportedOperationException("Managing favorite folders via HTML client is not currently supported. Use JmApiClient instead.");
HttpUrl.Builder urlBuilder = newHttpUrlBuilder()
.addPathSegment("user")
.addPathSegment(getLoggedInUserName())
.addPathSegment("favorite")
.addPathSegment("albums");

FormBody.Builder formBuilder = new FormBody.Builder();

if (type == FavoriteFolderType.ADD) {
formBuilder.add("addfolder-name", folderName);
}

if (type == FavoriteFolderType.DELETE) {
formBuilder.add("deletefolder-name", folderId);
}

if (type == FavoriteFolderType.EDIT) {
urlBuilder.addQueryParameter("folder", folderId);
formBuilder.add("editfolder-fid", folderId);
formBuilder.add("editfolder-name", folderName);
}

if (type == FavoriteFolderType.MOVE) {
formBuilder.add("movefolder-fid", folderId);
formBuilder.add("movefolder-aid", albumId);
}

JmHtmlResponse jmHtmlResponse = executePostRequest(urlBuilder.build(), formBuilder.build());

return new JmFavoriteFolderResult("ok", type.getDescription()+"成功");
}

// == HTML 客户端暂不实现(使用 JmApiClient) ==
Expand All @@ -468,7 +527,11 @@ public Map register(String username, String password, String passwordConfirm, St

@Override
public void logout() {
throw new UnsupportedOperationException("Logout via HTML client is not currently supported. Use JmApiClient instead.");
HttpUrl url = newHttpUrlBuilder()
.addPathSegment("logout")
.build();

JmHtmlResponse jmHtmlResponse = executeGetRequest(url);
Comment on lines +530 to +534

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'cacheUsername|getLoggedInUserName|getCachedJmFavoritePage|cacheJmFavoritePage|clear.*(Cache|Username|User)' \
  jmcomic-core/src/main/java

Repository: JUKOMU/JMComic-Api-Java

Length of output: 22912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- AbstractJmClient session/cache fields and methods ---\n'
sed -n '1,130p;880,1025p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java | cat -n

printf '\n--- JmHtmlClient logout and login/logout declarations ---\n'
sed -n '210,240p;520-545p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n

printf '\n--- logout implementation across core clients ---\n'
rg -n -C 5 'logout\(|clear.*Cache|username|FavoriteQuery|FolderId|Folder' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl

Repository: JUKOMU/JMComic-Api-Java

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- AbstractJmClient session/cache fields and methods ---'
sed -n '1,140p;880,1030p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java | cat -n

printf '%s\n' ''
printf '%s\n' '--- JmHtmlClient login/logout snippets ---'
sed -n '210,240p;520,545p' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java | cat -n

printf '%s\n' ''
printf '%s\n' '--- logout/user/cache usages in core client files ---'
rg -n -C 5 'logout\(|clear.*Cache|username|clear.*username|FavoriteQuery|FolderId|Folder|isBlank\(.*username|loggedInUserName' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl

Repository: JUKOMU/JMComic-Api-Java

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- watchHistory/cache references around getWatchHistory ---'
rg -n -C 6 'getWatchHistory|watchHistory|cache.*Favorite|Favorite.*cache|cache.*History|History.*cache' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/AbstractJmClient.java jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java

printf '%s\n' ''
printf '%s\n' '--- cachePool operations in AbstractJmClient/java impls ---'
rg -n -C 2 'cachePool\.(put|remove|clear|get)|new EmptyCookieJar|CookieManager|saveFromResponse|clearAll|evictAll|logout\(\{|\blog\(' jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client jmcomic-core/src/main/java/org/apache/http/client

Repository: JUKOMU/JMComic-Api-Java

Length of output: 19425


登出后清理本地会话状态。

login() 会调用 cacheUsername(username),但 logout() 只请求 /logout。登出后,同一客户端仍可使用旧用户名请求收藏夹或观看历史,并读取对应的收藏夹缓存。

logout() 中清理用户名状态及用户相关缓存;若现有 API 不支持,则在基类/HTML 客户端补充清理接口。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java`
around lines 530 - 534, 更新 JmHtmlClient.logout():成功完成 /logout 请求后清理
cacheUsername(username) 保存的当前用户名状态及该用户相关的收藏夹、观看历史缓存,确保后续请求不会复用旧会话数据。若现有清理 API
不足,在基类或 HTML 客户端补充并复用统一的会话清理接口;保留登出请求本身的现有流程。

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,9 @@ public static JmAlbum parseAlbum(String html) {
parseRelatedAlbums(doc),
// 章节列表
parsePhotoMetas(doc, id),
// HTML 解析不支持以下字段
"0", // seriesId
false, // isFavorite
false, // liked
parseFavorite(doc, id), // isFavorite
parseLiked(doc, id), // liked
false, // isAids
Collections.emptyList(), // images
"", // price
Expand All @@ -105,6 +104,11 @@ private static String decodeBase64Html(String html) {
}

private static String parseAlbumId(Document doc) {
Element albumIdElement = doc.getElementById("album_id");
if (albumIdElement != null && StringUtils.isNotBlank(albumIdElement.attr("value"))) {
return albumIdElement.attr("value");
}

// 优先从PC布局的 h2 标签提取
Element h2Element = doc.selectFirst("div.col-lg-7 h2:contains(禁漫车:), div.col-lg-7 h2:contains(禁漫車:)");
if (h2Element != null && h2Element.parent() != null) {
Expand All @@ -123,6 +127,30 @@ private static String parseAlbumId(Document doc) {
throw new ParseResponseException("Could not parse album id.");
}

private static boolean parseFavorite(Document doc, String albumId) {
Element favoriteElement = doc.getElementById("favorite_album_" + albumId);
if (favoriteElement == null) {
return false;
}

Element favoriteIcon = favoriteElement.selectFirst("i");
return favoriteIcon != null && !hasInlineStyle(favoriteIcon, "color", "#000000");
}
Comment on lines +130 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

不要把“不是黑色”直接判定为已收藏。

Line 137 在以下情况下会返回 true

  • 图标没有 style 属性。
  • style 存在,但颜色不是精确的 #000000
  • 颜色格式发生变化,例如短十六进制或 rgb(...)

这些情况只能说明“没有匹配到未收藏样式”,不能证明“已收藏”。请使用已确认的收藏状态正向标记;至少应把缺少或无法识别的样式判定为 false

最低限度的防误判修复
         Element favoriteIcon = favoriteElement.selectFirst("i");
-        return favoriteIcon != null && !hasInlineStyle(favoriteIcon, "color", "`#000000`");
+        return favoriteIcon != null
+                && StringUtils.isNotBlank(favoriteIcon.attr("style"))
+                && !hasInlineStyle(favoriteIcon, "color", "`#000000`");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/parser/HtmlParser.java`
around lines 130 - 138, Update parseFavorite to require a positively recognized
collected-state marker instead of treating any icon that is not exactly black as
favorited. Ensure missing, unrecognized, or differently formatted
styles—including absent style attributes, short hex, and rgb values—return
false, while preserving true only for the confirmed favorite marker.


private static boolean parseLiked(Document doc, String albumId) {
return doc.select("[id=love_likes_" + albumId + "] i")
.stream()
.anyMatch(element -> hasInlineStyle(element, "color", "red"));
}

private static boolean hasInlineStyle(Element element, String property, String value) {
return Arrays.stream(element.attr("style").split(";"))
.map(declaration -> declaration.split(":", 2))
.anyMatch(parts -> parts.length == 2
&& property.equalsIgnoreCase(parts[0].trim())
&& value.equalsIgnoreCase(parts[1].trim()));
}

private static String extractDate(Document doc, String key) {
// 优先尝试PC端结构
Element pcDateSpan = doc.selectFirst(String.format("div.col-lg-7 span:contains(%s)", key));
Expand Down
Loading