From 2f9626a2f788ad2fb02ab287c0c000233a42ddd7 Mon Sep 17 00:00:00 2001 From: manti <133025162+manti-X@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:14:14 +0100 Subject: [PATCH] Magazine Pocket: fix descrambler and refactor (#11687) * fix descrambler and refactor * getChapterUrl * toSChapter --- src/ja/magazinepocket/build.gradle | 2 +- .../extension/ja/magazinepocket/Dto.kt | 95 +++-- .../ja/magazinepocket/ImageInterceptor.kt | 42 ++- .../ja/magazinepocket/MagazinePocket.kt | 325 ++++++++---------- 4 files changed, 237 insertions(+), 227 deletions(-) diff --git a/src/ja/magazinepocket/build.gradle b/src/ja/magazinepocket/build.gradle index 3216b4e3e..9caf062c4 100644 --- a/src/ja/magazinepocket/build.gradle +++ b/src/ja/magazinepocket/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Magazine Pocket' extClass = '.MagazinePocket' - extVersionCode = 9 + extVersionCode = 10 isNsfw = false } diff --git a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/Dto.kt b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/Dto.kt index 13e636954..2935c716b 100644 --- a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/Dto.kt +++ b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/Dto.kt @@ -1,41 +1,46 @@ package eu.kanade.tachiyomi.extension.ja.magazinepocket +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames +import java.text.SimpleDateFormat @Serializable class RankingApiResponse( - @SerialName("ranking_title_list") val rankingTitleList: List, + @JsonNames("title_list", "ranking_title_list") + val rankingTitleList: List, ) @Serializable class RankingTitleId( + @JsonNames("title_id") val id: Int, ) @Serializable class TitleListResponse( - @SerialName("title_list") val titleList: List, + @JsonNames("title_list", "search_title_list") + val titleList: List, ) @Serializable class TitleDetail( - @SerialName("title_id") val titleId: Int, - @SerialName("title_name") val titleName: String, - @SerialName("thumbnail_image_url") val thumbnailImageUrl: String? = null, -) - -@Serializable -class LatestTitleListResponse( - @SerialName("title_list") val titleList: List, -) - -@Serializable -class LatestTitleDetail( - @SerialName("title_id") val titleId: Int, - @SerialName("title_name") val titleName: String, - @SerialName("thumbnail_rect_image_url") val thumbnailImageUrl: String? = null, -) + @SerialName("title_id") private val titleId: Int, + @SerialName("title_name") private val titleName: String, + @SerialName("thumbnail_image_url") private val thumbnailImageUrl: String? = null, + @SerialName("banner_image_url") private val bannerImageUrl: String? = null, + @SerialName("thumbnail_rect_image_url") private val thumbnailRectImageUrl: String? = null, +) { + fun toSManga(): SManga = SManga.create().apply { + val paddedId = titleId.toString().padStart(5, '0') + url = "/title/$paddedId" + title = titleName + thumbnail_url = thumbnailImageUrl ?: bannerImageUrl ?: thumbnailRectImageUrl + } +} @Serializable class EpisodeListResponse( @@ -44,13 +49,43 @@ class EpisodeListResponse( @Serializable class Episode( - @SerialName("episode_id") val episodeId: Int, - @SerialName("episode_name") val episodeName: String, - @SerialName("start_time") val startTime: String, - val point: Int, - @SerialName("title_id") val titleId: Int, - val badge: Int, - @SerialName("rental_finish_time") val rentalFinishTime: String? = null, + @SerialName("episode_id") private val episodeId: Int, + @SerialName("episode_name") private val episodeName: String, + private val index: Int, + @SerialName("start_time") private val startTime: String, + private val point: Int, + @SerialName("title_id") private val titleId: Int, + private val badge: Int, + @SerialName("rental_finish_time") private val rentalFinishTime: String? = null, +) { + fun toSChapter(dateFormat: SimpleDateFormat): SChapter = SChapter.create().apply { + val paddedId = titleId.toString().padStart(5, '0') + url = "/title/$paddedId/episode/$episodeId" + name = if (point > 0 && badge != 3 && rentalFinishTime == null) { + "🔒 $episodeName" + } else { + episodeName + } + chapter_number = index.toFloat() + date_upload = dateFormat.tryParse(startTime) + } +} + +@Serializable +class DetailResponse( + @SerialName("web_title") val webTitle: WebTitle, +) + +@Serializable +class WebTitle( + @SerialName("title_name") val titleName: String, + @SerialName("author_text") val authorText: String, + @SerialName("introduction_text") val introductionText: String, + @SerialName("genre_id_list") val genreIdList: List, + @SerialName("episode_id_list") val episodeIdList: List, + @SerialName("thumbnail_image_url") val thumbnailImageUrl: String? = null, + @SerialName("thumbnail_rect_image_url") val thumbnailRectImageUrl: String? = null, + @SerialName("banner_image_url") val bannerImageUrl: String? = null, ) @Serializable @@ -60,13 +95,11 @@ class ViewerApiResponse( ) @Serializable -class SearchApiResponse( - @SerialName("search_title_list") val searchTitleList: List, +class GenreListResponse( + @SerialName("genre_list") val genreList: List, ) @Serializable -class SearchTitleDetail( - @SerialName("title_id") val titleId: Int, - @SerialName("title_name") val titleName: String, - @SerialName("banner_image_url") val bannerImageUrl: String? = null, +class GenreDetail( + @SerialName("genre_name") val genreName: String, ) diff --git a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/ImageInterceptor.kt b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/ImageInterceptor.kt index 22901bbc0..23073643f 100644 --- a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/ImageInterceptor.kt +++ b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/ImageInterceptor.kt @@ -21,7 +21,14 @@ class ImageInterceptor : Interceptor { val seed = fragment.substringAfter("scramble_seed=").toLong() val response = chain.proceed(request) - val descrambledBody = descrambleImage(response.body, seed) + val imageBytes = response.body.bytes() + + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) + + val version = if (options.outHeight == 1600 || options.outHeight == 1024) 2 else 1 + val originalBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val descrambledBody = descrambleImage(originalBitmap, seed, version) return response.newBuilder().body(descrambledBody).build() } @@ -58,10 +65,8 @@ class ImageInterceptor : Interceptor { } } - private fun descrambleImage(responseBody: ResponseBody, seed: Long): ResponseBody { + private fun descrambleImage(originalBitmap: Bitmap, seed: Long, version: Int): ResponseBody { val unscrambledCoords = getUnscrambledCoords(seed) - val originalBitmap = BitmapFactory.decodeStream(responseBody.byteStream()) - ?: throw Exception("Failed to decode image stream") val originalWidth = originalBitmap.width val originalHeight = originalBitmap.height @@ -69,9 +74,16 @@ class ImageInterceptor : Interceptor { val descrambledBitmap = Bitmap.createBitmap(originalWidth, originalHeight, originalBitmap.config) val canvas = Canvas(descrambledBitmap) - val getTileDimension = { size: Int -> (size / 8 * 8) / 4 } - val tileWidth = getTileDimension(originalWidth) - val tileHeight = getTileDimension(originalHeight) + val (tileWidth, tileHeight) = when (version) { + 2 -> { + val getTile = { size: Int -> (size / 32) * 8 } + Pair(getTile(originalWidth), getTile(originalHeight)) + } + else -> { + val getTile = { size: Int -> (size / 8 * 8) / 4 } + Pair(getTile(originalWidth), getTile(originalHeight)) + } + } unscrambledCoords.forEach { coord -> val sx = coord.source.x * tileWidth @@ -84,6 +96,22 @@ class ImageInterceptor : Interceptor { canvas.drawBitmap(originalBitmap, srcRect, destRect, null) } + + if (version == 2) { + val processedWidth = tileWidth * 4 + val processedHeight = tileHeight * 4 + if (originalWidth > processedWidth) { + val srcRect = Rect(processedWidth, 0, originalWidth, originalHeight) + val destRect = Rect(processedWidth, 0, originalWidth, originalHeight) + canvas.drawBitmap(originalBitmap, srcRect, destRect, null) + } + if (originalHeight > processedHeight) { + val srcRect = Rect(0, processedHeight, processedWidth, originalHeight) + val destRect = Rect(0, processedHeight, processedWidth, originalHeight) + canvas.drawBitmap(originalBitmap, srcRect, destRect, null) + } + } + originalBitmap.recycle() val outputStream = ByteArrayOutputStream() diff --git a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/MagazinePocket.kt b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/MagazinePocket.kt index 0ef35f98f..52ba25060 100644 --- a/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/MagazinePocket.kt +++ b/src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/MagazinePocket.kt @@ -10,16 +10,8 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.asJsoup import keiyoushi.utils.firstInstance import keiyoushi.utils.parseAs -import keiyoushi.utils.tryParse -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import okhttp3.FormBody import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -66,6 +58,7 @@ class MagazinePocket : HttpSource() { } override fun popularMangaParse(response: Response): MangasPage { + val requestUrl = response.request.url.toString() val rankingResult = response.parseAs() val titleIds = rankingResult.rankingTitleList.map { it.id.toString().padStart(5, '0') } if (titleIds.isEmpty()) { @@ -87,13 +80,10 @@ class MagazinePocket : HttpSource() { } val detailsResult = detailsResponse.parseAs() - val mangas = detailsResult.titleList.map { manga -> - SManga.create().apply { - val paddedId = manga.titleId.toString().padStart(5, '0') - url = "/title/$paddedId" - title = manga.titleName - thumbnail_url = manga.thumbnailImageUrl - } + val mangas = detailsResult.titleList.map { it.toSManga() } + + if (requestUrl.contains("/genre/")) { + return MangasPage(mangas.reversed(), hasNextPage) } return MangasPage(mangas, hasNextPage) } @@ -122,16 +112,9 @@ class MagazinePocket : HttpSource() { } override fun latestUpdatesParse(response: Response): MangasPage { - val result = response.parseAs() - val manga = result.titleList.map { manga -> - SManga.create().apply { - val paddedId = manga.titleId.toString().padStart(5, '0') - url = "/title/$paddedId" - title = manga.titleName - thumbnail_url = manga.thumbnailImageUrl - } - } - return MangasPage(manga, true) + val result = response.parseAs() + val mangas = result.titleList.map { it.toSManga() } + return MangasPage(mangas, true) } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { @@ -147,119 +130,90 @@ class MagazinePocket : HttpSource() { val genreFilter = filters.firstInstance() val uriPart = genreFilter.toUriPart() - if (uriPart.startsWith("/search/genre/")) { - val url = baseUrl.toHttpUrl().newBuilder() - .addPathSegments(uriPart.removePrefix("/")) + val url = if (uriPart.contains("/genre/")) { + val genreId = uriPart.substringAfter("/genre/") + apiUrl.toHttpUrl().newBuilder() + .addPathSegments("search/title") + .addQueryParameter("platform", "3") + .addQueryParameter("genre_id", genreId) + .addQueryParameter("limit", "99999") + .build() + } else { + val rankingId = uriPart.substringAfter("/ranking/") + val offset = (page - 1) * pageLimit + apiUrl.toHttpUrl().newBuilder() + .addPathSegments("ranking/all") + .addQueryParameter("platform", "3") + .addQueryParameter("ranking_id", rankingId) + .addQueryParameter("offset", offset.toString()) + .addQueryParameter("limit", "26") .build() - return GET(url, headers) } - - val rankingId = uriPart.substringAfter("/ranking/") - val offset = (page - 1) * pageLimit - val url = apiUrl.toHttpUrl().newBuilder() - .addPathSegments("ranking/all") - .addQueryParameter("platform", "3") - .addQueryParameter("ranking_id", rankingId) - .addQueryParameter("offset", offset.toString()) - .addQueryParameter("limit", "26") - .build() return hashedGet(url) } override fun searchMangaParse(response: Response): MangasPage { val requestUrl = response.request.url.toString() if (requestUrl.contains("/web/search/")) { - val result = response.parseAs() - val mangas = result.searchTitleList.map { manga -> - SManga.create().apply { - val paddedId = manga.titleId.toString().padStart(5, '0') - url = "/title/$paddedId" - title = manga.titleName - thumbnail_url = manga.bannerImageUrl - } - } - return MangasPage(mangas.reversed(), false) - } - - if (response.request.url.toString().contains("/search/genre/")) { - val document = response.asJsoup() - val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() - ?: return MangasPage(emptyList(), false) - - val rootArray = nuxtData.parseAs() - fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] - - val genreResultObject = rootArray.firstOrNull { it is JsonObject && "search_title_list" in it.jsonObject } - ?: return MangasPage(emptyList(), false) - - val mangaRefs = resolve(genreResultObject.jsonObject["search_title_list"]!!).jsonArray - - val mangas = mangaRefs.map { ref -> - val mangaObject = resolve(ref).jsonObject - val id = resolve(mangaObject["title_id"]!!).jsonPrimitive - SManga.create().apply { - val paddedId = id.toString().padStart(5, '0') - url = "/title/$paddedId" - title = resolve(mangaObject["title_name"]!!).jsonPrimitive.content - thumbnail_url = mangaObject["banner_image_url"]?.let { resolve(it).jsonPrimitive.content } - } - } + val result = response.parseAs() + val mangas = result.titleList.map { it.toSManga() } return MangasPage(mangas, false) } return popularMangaParse(response) } // Details + override fun getMangaUrl(manga: SManga): String { + return baseUrl + manga.url + } + + override fun mangaDetailsRequest(manga: SManga): Request { + val titleId = manga.url.substringAfter("/title/") + val url = apiUrl.toHttpUrl().newBuilder() + .addPathSegments("web/title/detail") + .addQueryParameter("platform", "3") + .addQueryParameter("title_id", titleId) + .build() + return hashedGet(url) + } + override fun mangaDetailsParse(response: Response): SManga { - val document = response.asJsoup() - val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() - ?: throw Exception("Could not find Nuxt data") - val rootArray = nuxtData.parseAs() + val result = response.parseAs().webTitle + return SManga.create().apply { + title = result.titleName + author = result.authorText + description = result.introductionText + thumbnail_url = result.thumbnailImageUrl ?: result.bannerImageUrl ?: result.thumbnailRectImageUrl + if (result.genreIdList.isNotEmpty()) { + val genreApiUrl = apiUrl.toHttpUrl().newBuilder() + .addPathSegments("genre/list") + .addQueryParameter("platform", "3") + .addQueryParameter("genre_id_list", result.genreIdList.joinToString(",")) + .build() - fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] + val genreRequest = hashedGet(genreApiUrl) + val genreResponse = client.newCall(genreRequest).execute() - val titleDetailsObject = rootArray - .filterIsInstance() - .findLast { "title_name" in it && "author_text" in it && "introduction_text" in it && "genre_id_list" in it } - ?.jsonObject - - val genreMap = buildMap { - rootArray.forEach { element -> - if (element is JsonObject && element.jsonObject.containsKey("genre_id")) { - val genreObject = element.jsonObject - val id = resolve(genreObject["genre_id"]!!).jsonPrimitive.content.toInt() - val name = resolve(genreObject["genre_name"]!!).jsonPrimitive.content - put(id, name) + if (genreResponse.isSuccessful) { + val genreResult = genreResponse.parseAs() + genre = genreResult.genreList.joinToString { it.genreName } } } } - - return SManga.create().apply { - title = resolve(titleDetailsObject?.get("title_name")!!).jsonPrimitive.content - author = resolve(titleDetailsObject["author_text"]!!).jsonPrimitive.content - description = resolve(titleDetailsObject["introduction_text"]!!).jsonPrimitive.content - val genreIdRefs = resolve(titleDetailsObject["genre_id_list"]!!).jsonArray - val genreIds = genreIdRefs.map { resolve(it).jsonPrimitive.content.toInt() } - genre = genreIds.mapNotNull { genreMap[it] }.joinToString() - } } // Chapters override fun chapterListRequest(manga: SManga): Request { - return GET(baseUrl + manga.url, headers) + return mangaDetailsRequest(manga) } override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() - ?: throw Exception("Could not find Nuxt data") + val resultIds = response.parseAs() + val episodeIds = resultIds.webTitle.episodeIdList.map { it.toString() } - val rootArray = nuxtData.parseAs() - fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] - - val titleDetailsObject = rootArray.first { it is JsonObject && it.jsonObject.containsKey("episode_id_list") }.jsonObject - val episodeIdRefs = resolve(titleDetailsObject["episode_id_list"]!!).jsonArray - val episodeIds = episodeIdRefs.map { resolve(it).jsonPrimitive.content } + if (episodeIds.isEmpty()) { + return emptyList() + } val formBody = FormBody.Builder() .add("platform", "3") @@ -284,18 +238,13 @@ class MagazinePocket : HttpSource() { val result = apiResponse.parseAs() - return result.episodeList.map { chapter -> - SChapter.create().apply { - val paddedId = chapter.titleId.toString().padStart(5, '0') - url = "/title/$paddedId/episode/${chapter.episodeId}" - name = if (chapter.point > 0 && chapter.badge != 3 && chapter.rentalFinishTime == null) { - "🔒 ${chapter.episodeName}" - } else { - chapter.episodeName - } - date_upload = dateFormat.tryParse(chapter.startTime) - } - }.reversed() + return result.episodeList + .map { it.toSChapter(dateFormat) } + .reversed() + } + + override fun getChapterUrl(chapter: SChapter): String { + return baseUrl + chapter.url } // Pages @@ -381,72 +330,72 @@ class MagazinePocket : HttpSource() { Pair("(ランキング) ドラマ", "/ranking/27"), Pair("(ランキング) ファンタジー", "/ranking/28"), Pair("(ランキング) 日常", "/ranking/29"), - Pair("恋愛・ラブコメ", "/search/genre/1"), - Pair("ホラー・ミステリー・サスペンス", "/search/genre/2"), - Pair("ギャグ・コメディー・日常", "/search/genre/3"), - Pair("SF・ファンタジー", "/search/genre/4"), - Pair("スポーツ", "/search/genre/5"), - Pair("ヒューマンドラマ", "/search/genre/6"), - Pair("裏社会・アングラ・ヤンキー", "/search/genre/7"), - Pair("アクション・バトル", "/search/genre/8"), - Pair("異世界・異能力", "/search/genre/9"), - Pair("読切", "/search/genre/10"), - Pair("MGP", "/search/genre/11"), - Pair("第98回新人漫画賞", "/search/genre/12"), - Pair("第99回新人漫画賞", "/search/genre/13"), - Pair("第100回新人漫画賞", "/search/genre/14"), - Pair("第101回新人漫画賞", "/search/genre/15"), - Pair("第102回新人漫画賞", "/search/genre/16"), - Pair("第103回新人漫画賞", "/search/genre/17"), - Pair("第104回新人漫画賞", "/search/genre/18"), - Pair("第105回新人漫画賞", "/search/genre/19"), - Pair("第106回新人漫画賞", "/search/genre/20"), - Pair("第107回新人漫画賞", "/search/genre/21"), - Pair("第108回新人漫画賞", "/search/genre/22"), - Pair("第109回新人漫画賞", "/search/genre/23"), - Pair("第110回新人漫画賞", "/search/genre/24"), - Pair("2023真夏の読み切り15連弾", "/search/genre/25"), - Pair("マガジンライズ", "/search/genre/26"), - Pair("第111回新人漫画賞", "/search/genre/27"), - Pair("少女/女性", "/search/genre/28"), - Pair("新人漫画大賞", "/search/genre/29"), - Pair("第75回新人漫画賞", "/search/genre/30"), - Pair("第79回新人漫画賞", "/search/genre/31"), - Pair("第85回新人漫画賞", "/search/genre/32"), - Pair("第88回新人漫画賞", "/search/genre/33"), - Pair("第89回新人漫画賞", "/search/genre/34"), - Pair("第91回新人漫画賞", "/search/genre/35"), - Pair("第92回新人漫画賞", "/search/genre/36"), - Pair("第94回新人漫画賞", "/search/genre/37"), - Pair("第95回新人漫画賞", "/search/genre/38"), - Pair("第96回新人漫画賞", "/search/genre/39"), - Pair("第97回新人漫画賞", "/search/genre/40"), - Pair("第112回新人漫画賞", "/search/genre/41"), - Pair("第113回新人漫画大賞", "/search/genre/42"), - Pair("サッカー", "/search/genre/43"), - Pair("テニス", "/search/genre/44"), - Pair("バスケ", "/search/genre/45"), - Pair("格闘技", "/search/genre/46"), - Pair("野球", "/search/genre/47"), - Pair("女性向け異世界", "/search/genre/48"), - Pair("アニメ化", "/search/genre/49"), - Pair("実写化", "/search/genre/50"), - Pair("車・バイク", "/search/genre/51"), - Pair("グルメ・料理", "/search/genre/52"), - Pair("医療", "/search/genre/53"), - Pair("頭脳戦", "/search/genre/54"), - Pair("サバイバル", "/search/genre/55"), - Pair("復讐劇", "/search/genre/56"), - Pair("70~80年代", "/search/genre/57"), - Pair("90年代", "/search/genre/58"), - Pair("金田一シリーズ", "/search/genre/59"), - Pair("第114回新人漫画大賞", "/search/genre/60"), - Pair("連載獲得ダービー", "/search/genre/61"), - Pair("有名漫画賞", "/search/genre/62"), - Pair("探偵・警察", "/search/genre/63"), - Pair("歴史・時代", "/search/genre/64"), - Pair("不倫・浮気", "/search/genre/65"), - Pair("犬・猫", "/search/genre/66"), + Pair("恋愛・ラブコメ", "/genre/1"), + Pair("ホラー・ミステリー・サスペンス", "/genre/2"), + Pair("ギャグ・コメディー・日常", "/genre/3"), + Pair("SF・ファンタジー", "/genre/4"), + Pair("スポーツ", "/genre/5"), + Pair("ヒューマンドラマ", "/genre/6"), + Pair("裏社会・アングラ・ヤンキー", "/genre/7"), + Pair("アクション・バトル", "/genre/8"), + Pair("異世界・異能力", "/genre/9"), + Pair("読切", "/genre/10"), + Pair("MGP", "/genre/11"), + Pair("第98回新人漫画賞", "/genre/12"), + Pair("第99回新人漫画賞", "/genre/13"), + Pair("第100回新人漫画賞", "/genre/14"), + Pair("第101回新人漫画賞", "/genre/15"), + Pair("第102回新人漫画賞", "/genre/16"), + Pair("第103回新人漫画賞", "/genre/17"), + Pair("第104回新人漫画賞", "/genre/18"), + Pair("第105回新人漫画賞", "/genre/19"), + Pair("第106回新人漫画賞", "/genre/20"), + Pair("第107回新人漫画賞", "/genre/21"), + Pair("第108回新人漫画賞", "/genre/22"), + Pair("第109回新人漫画賞", "/genre/23"), + Pair("第110回新人漫画賞", "/genre/24"), + Pair("2023真夏の読み切り15連弾", "/genre/25"), + Pair("マガジンライズ", "/genre/26"), + Pair("第111回新人漫画賞", "/genre/27"), + Pair("少女/女性", "/genre/28"), + Pair("新人漫画大賞", "/genre/29"), + Pair("第75回新人漫画賞", "/genre/30"), + Pair("第79回新人漫画賞", "/genre/31"), + Pair("第85回新人漫画賞", "/genre/32"), + Pair("第88回新人漫画賞", "/genre/33"), + Pair("第89回新人漫画賞", "/genre/34"), + Pair("第91回新人漫画賞", "/genre/35"), + Pair("第92回新人漫画賞", "/genre/36"), + Pair("第94回新人漫画賞", "/genre/37"), + Pair("第95回新人漫画賞", "/genre/38"), + Pair("第96回新人漫画賞", "/genre/39"), + Pair("第97回新人漫画賞", "/genre/40"), + Pair("第112回新人漫画賞", "/genre/41"), + Pair("第113回新人漫画大賞", "/genre/42"), + Pair("サッカー", "/genre/43"), + Pair("テニス", "/genre/44"), + Pair("バスケ", "/genre/45"), + Pair("格闘技", "/genre/46"), + Pair("野球", "/genre/47"), + Pair("女性向け異世界", "/genre/48"), + Pair("アニメ化", "/genre/49"), + Pair("実写化", "/genre/50"), + Pair("車・バイク", "/genre/51"), + Pair("グルメ・料理", "/genre/52"), + Pair("医療", "/genre/53"), + Pair("頭脳戦", "/genre/54"), + Pair("サバイバル", "/genre/55"), + Pair("復讐劇", "/genre/56"), + Pair("70~80年代", "/genre/57"), + Pair("90年代", "/genre/58"), + Pair("金田一シリーズ", "/genre/59"), + Pair("第114回新人漫画大賞", "/genre/60"), + Pair("連載獲得ダービー", "/genre/61"), + Pair("有名漫画賞", "/genre/62"), + Pair("探偵・警察", "/genre/63"), + Pair("歴史・時代", "/genre/64"), + Pair("不倫・浮気", "/genre/65"), + Pair("犬・猫", "/genre/66"), ) // Unsupported