diff --git a/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/client/JmClient.java b/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/client/JmClient.java index df00ab0..6121271 100644 --- a/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/client/JmClient.java +++ b/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/client/JmClient.java @@ -308,11 +308,20 @@ public interface JmClient { List getRandomRecommend(); /** - * 获取首页推广/Banner内容 + * 获取首页推荐栏分类 * - * @return 推广数据(原始JSON Map) + * @return 首页推荐栏分类列表 */ - Map getPromote(); + List getPromote(); + + /** + * 获取首页推荐栏分类详情(分页) + * + * @param category 推荐栏分类 + * @param page 页码(从1开始) + * @return 分页结果 + */ + JmSearchPage getPromoteList(JmPromoteCategory category, int page); /** * 获取连载/系列化列表(分页) diff --git a/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/model/JmPromoteCategory.java b/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/model/JmPromoteCategory.java new file mode 100644 index 0000000..c8cd882 --- /dev/null +++ b/jmcomic-api/src/main/java/io/github/jukomu/jmcomic/api/model/JmPromoteCategory.java @@ -0,0 +1,62 @@ +package io.github.jukomu.jmcomic.api.model; + +import java.util.List; +import java.util.Map; + +/** + * @author JUKOMU + * @Description: 首页推荐栏分类 + * @Project: jmcomic-api-java + * @Date: 2026/07/13 + */ +public record JmPromoteCategory( + /* + 分类ID + */ + String id, + /* + 分类标题 + */ + String title, + /* + 分类别名 + */ + String slug, + /* + 分类类型(promote, not_in_category_id, category_id, library, novels) + */ + String type, + /* + 筛选值 + */ + String filterVal, + /* + 内容列表(不同type的content结构不同,故使用Map) + */ + List content +) { + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getSlug() { + return slug; + } + + public String getType() { + return type; + } + + public String getFilterVal() { + return filterVal; + } + + public List getContent() { + return content; + } +} diff --git a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmApiClient.java b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmApiClient.java index 75da155..67801e1 100644 --- a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmApiClient.java +++ b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmApiClient.java @@ -3,9 +3,7 @@ import com.google.gson.*; import io.github.jukomu.jmcomic.api.client.JmCreatorClient; import io.github.jukomu.jmcomic.api.client.JmNovelClient; -import io.github.jukomu.jmcomic.api.enums.FavoriteFolderType; -import io.github.jukomu.jmcomic.api.enums.TimeOption; -import io.github.jukomu.jmcomic.api.enums.VoteType; +import io.github.jukomu.jmcomic.api.enums.*; import io.github.jukomu.jmcomic.api.exception.*; import io.github.jukomu.jmcomic.api.model.*; import io.github.jukomu.jmcomic.core.client.AbstractJmClient; @@ -564,7 +562,7 @@ public JmSearchPage getLatest(int page) { .build(); JmApiResponse jmApiResponse = executeGetRequest(url, JmConstants.APP_TOKEN_SECRET); - return ApiParser.parseLatestOrPromoteList(jmApiResponse.getDecodedData(), page); + return ApiParser.parseLatestList(jmApiResponse.getDecodedData(), page); } @Override @@ -719,15 +717,57 @@ public List getRandomRecommend() { } @Override - public Map getPromote() { + public List getPromote() { HttpUrl url = newHttpUrlBuilder() .addPathSegment(JmConstants.API_COMIC_PROMOTE) .build(); JmApiResponse jmApiResponse = executeGetRequest(url, JmConstants.APP_TOKEN_SECRET); - // API 返回的是数组,接口要 Map,包一层 {"list": [...]} 兼容一下 - List list = JsonUtils.fromJson(jmApiResponse.getDecodedData(), List.class); - return Map.of("list", list); + return ApiParser.parsePromote(jmApiResponse.getDecodedData()); + } + + @Override + public JmSearchPage getPromoteList(JmPromoteCategory category, int page) { + switch (category.getType()) { + case "promote": { + HttpUrl url = newHttpUrlBuilder() + .addPathSegment(JmConstants.API_COMIC_PROMOTE_LIST) + .addQueryParameter("id", category.getId()) + .addQueryParameter("page", String.valueOf(page)) + .build(); + JmApiResponse jmApiResponse = executeGetRequest(url, JmConstants.APP_TOKEN_SECRET); + return ApiParser.parsePromoteList(jmApiResponse.getDecodedData(), page); + } + case "category_id": { + Category cat = null; + for (Category c : Category.values()) { + if (c.getValue().equals(category.getSlug())) { + cat = c; + break; + } + } + if (cat == null) { + throw new UnsupportedOperationException("Unknown category slug: " + category.getSlug()); + } + SearchQuery searchQuery = new SearchQuery.Builder() + .category(cat) + .page(page) + .orderBy(OrderBy.LATEST) + .build(); + return getCategories(searchQuery); + } + case "not_in_category_id": { + SearchQuery searchQuery = new SearchQuery.Builder() + .text(category.getTitle()) + .mainTag(SearchMainTag.SITE_SEARCH) + .page(page) + .orderBy(OrderBy.LATEST) + .build(); + return search(searchQuery); + } + default: + throw new UnsupportedOperationException("Promote type '" + category.getType() + "' is not supported"); + } } @Override diff --git a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java index 198736e..0ed8f6b 100644 --- a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java +++ b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/client/impl/JmHtmlClient.java @@ -491,7 +491,12 @@ public List getRandomRecommend() { } @Override - public Map getPromote() { + public List getPromote() { + throw new UnsupportedOperationException("Get promote via HTML client is not currently supported. Use JmApiClient instead."); + } + + @Override + public JmSearchPage getPromoteList(JmPromoteCategory category, int page) { throw new UnsupportedOperationException("Get promote via HTML client is not currently supported. Use JmApiClient instead."); } diff --git a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/parser/ApiParser.java b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/parser/ApiParser.java index c427ea7..d33edcc 100644 --- a/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/parser/ApiParser.java +++ b/jmcomic-core/src/main/java/io/github/jukomu/jmcomic/core/parser/ApiParser.java @@ -8,6 +8,7 @@ import io.github.jukomu.jmcomic.api.exception.ResponseException; import io.github.jukomu.jmcomic.api.model.*; import io.github.jukomu.jmcomic.core.constant.JmConstants; +import io.github.jukomu.jmcomic.core.util.JsonUtils; import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Entities; @@ -522,14 +523,14 @@ public static JmSearchPage parseSearchPage(String jsonStr, int currentPage) { } /** - * 解析最新上架/推广列表 (Latest/Promote List) 的API JSON响应。 + * 解析最新上架的API JSON响应。 * 这些 API 返回的是裸 JSON 数组 [{...}, {...}],而非包装对象。 * * @param jsonStr API返回的JSON字符串(裸数组)。 * @param currentPage 当前页码。 * @return 一个 JmSearchPage 对象。 */ - public static JmSearchPage parseLatestOrPromoteList(String jsonStr, int currentPage) { + public static JmSearchPage parseLatestList(String jsonStr, int currentPage) { try { JsonArray array = JsonParser.parseString(jsonStr).getAsJsonArray(); List list = parseAlbumMetaList(array); @@ -543,7 +544,7 @@ public static JmSearchPage parseLatestOrPromoteList(String jsonStr, int currentP } return new JmSearchPage(currentPage, totalItems, totalPages, list); } catch (Exception e) { - throw new ParseResponseException("Failed to parse latest/promote list API JSON", e); + throw new ParseResponseException("Failed to parse latest list API JSON", e); } } @@ -592,6 +593,75 @@ public static List parseRandomRecommendList(String jsonStr) { } } + /** + * 解析首页推荐栏分类 (Promote) 的API JSON响应。 + * 该接口返回裸 JSON 数组 [{...}, {...}],每个元素包含 id, title, slug, type, filter_val, content。 + * + * @param jsonStr API返回的JSON字符串。 + * @return JmPromoteCategory 列表。 + */ + public static List parsePromote(String jsonStr) { + try { + JsonArray array = JsonParser.parseString(jsonStr).getAsJsonArray(); + List result = new ArrayList<>(); + for (JsonElement item : array) { + JsonObject node = item.getAsJsonObject(); + List content = JsonUtils.fromJson( + node.getAsJsonArray("content").toString(), List.class); + result.add(new JmPromoteCategory( + getString(node, "id"), + getString(node, "title"), + getString(node, "slug"), + getString(node, "type"), + getString(node, "filter_val"), + content + )); + } + return result; + } catch (Exception e) { + throw new ParseResponseException("Failed to parse promote API JSON", e); + } + } + + /** + * 解析首页推荐栏列表 (Promote List) 的API JSON响应。 + * 该接口返回结构为 {@code {"total": "140", "list": [...]}},total 为字符串,内容在 list 字段。 + * + * @param jsonStr API返回的JSON字符串。 + * @param currentPage 当前页码。 + * @return 一个 JmSearchPage 对象。 + */ + public static JmSearchPage parsePromoteList(String jsonStr, int currentPage) { + try { + JsonObject jsonObject = JsonParser.parseString(jsonStr).getAsJsonObject(); + + int totalItems = 0; + if (jsonObject.has("total") && !jsonObject.get("total").isJsonNull()) { + JsonElement totalElement = jsonObject.get("total"); + if (totalElement.isJsonPrimitive() && totalElement.getAsJsonPrimitive().isString()) { + totalItems = Integer.parseInt(totalElement.getAsString()); + } else { + totalItems = totalElement.getAsInt(); + } + } + + int totalPages = (totalItems == 0) + ? 0 + : (int) Math.ceil((double) totalItems / JmConstants.PAGE_SIZE_SEARCH); + + JsonArray listArray = jsonObject.has("list") && jsonObject.get("list").isJsonArray() + ? jsonObject.getAsJsonArray("list") + : new JsonArray(); + List content = parseAlbumMetaList(listArray); + + return new JmSearchPage(currentPage, totalItems, totalPages, content); + } catch (ParseResponseException e) { + throw e; + } catch (Exception e) { + throw new ParseResponseException("Failed to parse promote list API JSON", e); + } + } + /** * 解析收藏标签列表的API JSON响应。 * 该接口返回结构为 {"list": [{"tag": "...", "updated_at": "..."}, ...]}。