Magazine Pocket: fix descrambler and refactor (#11687)

* fix descrambler and refactor

* getChapterUrl

* toSChapter
This commit is contained in:
manti 2025-11-17 13:14:14 +01:00 committed by Draff
parent b1ee9c1589
commit 2f9626a2f7
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 237 additions and 227 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Magazine Pocket'
extClass = '.MagazinePocket'
extVersionCode = 9
extVersionCode = 10
isNsfw = false
}

View File

@ -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<RankingTitleId>,
@JsonNames("title_list", "ranking_title_list")
val rankingTitleList: List<RankingTitleId>,
)
@Serializable
class RankingTitleId(
@JsonNames("title_id")
val id: Int,
)
@Serializable
class TitleListResponse(
@SerialName("title_list") val titleList: List<TitleDetail>,
@JsonNames("title_list", "search_title_list")
val titleList: List<TitleDetail>,
)
@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<LatestTitleDetail>,
)
@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<Int>,
@SerialName("episode_id_list") val episodeIdList: List<Int>,
@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<SearchTitleDetail>,
class GenreListResponse(
@SerialName("genre_list") val genreList: List<GenreDetail>,
)
@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,
)

View File

@ -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()

View File

@ -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<RankingApiResponse>()
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<TitleListResponse>()
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<LatestTitleListResponse>()
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<TitleListResponse>()
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<GenreFilter>()
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<SearchApiResponse>()
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<JsonArray>()
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<TitleListResponse>()
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<JsonArray>()
val result = response.parseAs<DetailResponse>().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<JsonObject>()
.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<GenreListResponse>()
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<SChapter> {
val document = response.asJsoup()
val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data()
?: throw Exception("Could not find Nuxt data")
val resultIds = response.parseAs<DetailResponse>()
val episodeIds = resultIds.webTitle.episodeIdList.map { it.toString() }
val rootArray = nuxtData.parseAs<JsonArray>()
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<EpisodeListResponse>()
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("7080年代", "/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("7080年代", "/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