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 { ext {
extName = 'Magazine Pocket' extName = 'Magazine Pocket'
extClass = '.MagazinePocket' extClass = '.MagazinePocket'
extVersionCode = 9 extVersionCode = 10
isNsfw = false isNsfw = false
} }

View File

@ -1,41 +1,46 @@
package eu.kanade.tachiyomi.extension.ja.magazinepocket 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.text.SimpleDateFormat
@Serializable @Serializable
class RankingApiResponse( class RankingApiResponse(
@SerialName("ranking_title_list") val rankingTitleList: List<RankingTitleId>, @JsonNames("title_list", "ranking_title_list")
val rankingTitleList: List<RankingTitleId>,
) )
@Serializable @Serializable
class RankingTitleId( class RankingTitleId(
@JsonNames("title_id")
val id: Int, val id: Int,
) )
@Serializable @Serializable
class TitleListResponse( class TitleListResponse(
@SerialName("title_list") val titleList: List<TitleDetail>, @JsonNames("title_list", "search_title_list")
val titleList: List<TitleDetail>,
) )
@Serializable @Serializable
class TitleDetail( class TitleDetail(
@SerialName("title_id") val titleId: Int, @SerialName("title_id") private val titleId: Int,
@SerialName("title_name") val titleName: String, @SerialName("title_name") private val titleName: String,
@SerialName("thumbnail_image_url") val thumbnailImageUrl: String? = null, @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,
@Serializable ) {
class LatestTitleListResponse( fun toSManga(): SManga = SManga.create().apply {
@SerialName("title_list") val titleList: List<LatestTitleDetail>, val paddedId = titleId.toString().padStart(5, '0')
) url = "/title/$paddedId"
title = titleName
@Serializable thumbnail_url = thumbnailImageUrl ?: bannerImageUrl ?: thumbnailRectImageUrl
class LatestTitleDetail( }
@SerialName("title_id") val titleId: Int, }
@SerialName("title_name") val titleName: String,
@SerialName("thumbnail_rect_image_url") val thumbnailImageUrl: String? = null,
)
@Serializable @Serializable
class EpisodeListResponse( class EpisodeListResponse(
@ -44,13 +49,43 @@ class EpisodeListResponse(
@Serializable @Serializable
class Episode( class Episode(
@SerialName("episode_id") val episodeId: Int, @SerialName("episode_id") private val episodeId: Int,
@SerialName("episode_name") val episodeName: String, @SerialName("episode_name") private val episodeName: String,
@SerialName("start_time") val startTime: String, private val index: Int,
val point: Int, @SerialName("start_time") private val startTime: String,
@SerialName("title_id") val titleId: Int, private val point: Int,
val badge: Int, @SerialName("title_id") private val titleId: Int,
@SerialName("rental_finish_time") val rentalFinishTime: String? = null, 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 @Serializable
@ -60,13 +95,11 @@ class ViewerApiResponse(
) )
@Serializable @Serializable
class SearchApiResponse( class GenreListResponse(
@SerialName("search_title_list") val searchTitleList: List<SearchTitleDetail>, @SerialName("genre_list") val genreList: List<GenreDetail>,
) )
@Serializable @Serializable
class SearchTitleDetail( class GenreDetail(
@SerialName("title_id") val titleId: Int, @SerialName("genre_name") val genreName: String,
@SerialName("title_name") val titleName: String,
@SerialName("banner_image_url") val bannerImageUrl: String? = null,
) )

View File

@ -21,7 +21,14 @@ class ImageInterceptor : Interceptor {
val seed = fragment.substringAfter("scramble_seed=").toLong() val seed = fragment.substringAfter("scramble_seed=").toLong()
val response = chain.proceed(request) 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() 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 unscrambledCoords = getUnscrambledCoords(seed)
val originalBitmap = BitmapFactory.decodeStream(responseBody.byteStream())
?: throw Exception("Failed to decode image stream")
val originalWidth = originalBitmap.width val originalWidth = originalBitmap.width
val originalHeight = originalBitmap.height val originalHeight = originalBitmap.height
@ -69,9 +74,16 @@ class ImageInterceptor : Interceptor {
val descrambledBitmap = Bitmap.createBitmap(originalWidth, originalHeight, originalBitmap.config) val descrambledBitmap = Bitmap.createBitmap(originalWidth, originalHeight, originalBitmap.config)
val canvas = Canvas(descrambledBitmap) val canvas = Canvas(descrambledBitmap)
val getTileDimension = { size: Int -> (size / 8 * 8) / 4 } val (tileWidth, tileHeight) = when (version) {
val tileWidth = getTileDimension(originalWidth) 2 -> {
val tileHeight = getTileDimension(originalHeight) 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 -> unscrambledCoords.forEach { coord ->
val sx = coord.source.x * tileWidth val sx = coord.source.x * tileWidth
@ -84,6 +96,22 @@ class ImageInterceptor : Interceptor {
canvas.drawBitmap(originalBitmap, srcRect, destRect, null) 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() originalBitmap.recycle()
val outputStream = ByteArrayOutputStream() 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance import keiyoushi.utils.firstInstance
import keiyoushi.utils.parseAs 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.FormBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -66,6 +58,7 @@ class MagazinePocket : HttpSource() {
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val requestUrl = response.request.url.toString()
val rankingResult = response.parseAs<RankingApiResponse>() val rankingResult = response.parseAs<RankingApiResponse>()
val titleIds = rankingResult.rankingTitleList.map { it.id.toString().padStart(5, '0') } val titleIds = rankingResult.rankingTitleList.map { it.id.toString().padStart(5, '0') }
if (titleIds.isEmpty()) { if (titleIds.isEmpty()) {
@ -87,13 +80,10 @@ class MagazinePocket : HttpSource() {
} }
val detailsResult = detailsResponse.parseAs<TitleListResponse>() val detailsResult = detailsResponse.parseAs<TitleListResponse>()
val mangas = detailsResult.titleList.map { manga -> val mangas = detailsResult.titleList.map { it.toSManga() }
SManga.create().apply {
val paddedId = manga.titleId.toString().padStart(5, '0') if (requestUrl.contains("/genre/")) {
url = "/title/$paddedId" return MangasPage(mangas.reversed(), hasNextPage)
title = manga.titleName
thumbnail_url = manga.thumbnailImageUrl
}
} }
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
@ -122,16 +112,9 @@ class MagazinePocket : HttpSource() {
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<LatestTitleListResponse>() val result = response.parseAs<TitleListResponse>()
val manga = result.titleList.map { manga -> val mangas = result.titleList.map { it.toSManga() }
SManga.create().apply { return MangasPage(mangas, true)
val paddedId = manga.titleId.toString().padStart(5, '0')
url = "/title/$paddedId"
title = manga.titleName
thumbnail_url = manga.thumbnailImageUrl
}
}
return MangasPage(manga, true)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -147,119 +130,90 @@ class MagazinePocket : HttpSource() {
val genreFilter = filters.firstInstance<GenreFilter>() val genreFilter = filters.firstInstance<GenreFilter>()
val uriPart = genreFilter.toUriPart() val uriPart = genreFilter.toUriPart()
if (uriPart.startsWith("/search/genre/")) { val url = if (uriPart.contains("/genre/")) {
val url = baseUrl.toHttpUrl().newBuilder() val genreId = uriPart.substringAfter("/genre/")
.addPathSegments(uriPart.removePrefix("/")) apiUrl.toHttpUrl().newBuilder()
.addPathSegments("search/title")
.addQueryParameter("platform", "3")
.addQueryParameter("genre_id", genreId)
.addQueryParameter("limit", "99999")
.build() .build()
return GET(url, headers) } else {
}
val rankingId = uriPart.substringAfter("/ranking/") val rankingId = uriPart.substringAfter("/ranking/")
val offset = (page - 1) * pageLimit val offset = (page - 1) * pageLimit
val url = apiUrl.toHttpUrl().newBuilder() apiUrl.toHttpUrl().newBuilder()
.addPathSegments("ranking/all") .addPathSegments("ranking/all")
.addQueryParameter("platform", "3") .addQueryParameter("platform", "3")
.addQueryParameter("ranking_id", rankingId) .addQueryParameter("ranking_id", rankingId)
.addQueryParameter("offset", offset.toString()) .addQueryParameter("offset", offset.toString())
.addQueryParameter("limit", "26") .addQueryParameter("limit", "26")
.build() .build()
}
return hashedGet(url) return hashedGet(url)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val requestUrl = response.request.url.toString() val requestUrl = response.request.url.toString()
if (requestUrl.contains("/web/search/")) { if (requestUrl.contains("/web/search/")) {
val result = response.parseAs<SearchApiResponse>() val result = response.parseAs<TitleListResponse>()
val mangas = result.searchTitleList.map { manga -> val mangas = result.titleList.map { it.toSManga() }
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 }
}
}
return MangasPage(mangas, false) return MangasPage(mangas, false)
} }
return popularMangaParse(response) return popularMangaParse(response)
} }
// Details // 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 { override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() val result = response.parseAs<DetailResponse>().webTitle
val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data()
?: throw Exception("Could not find Nuxt data")
val rootArray = nuxtData.parseAs<JsonArray>()
fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()]
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)
}
}
}
return SManga.create().apply { return SManga.create().apply {
title = resolve(titleDetailsObject?.get("title_name")!!).jsonPrimitive.content title = result.titleName
author = resolve(titleDetailsObject["author_text"]!!).jsonPrimitive.content author = result.authorText
description = resolve(titleDetailsObject["introduction_text"]!!).jsonPrimitive.content description = result.introductionText
val genreIdRefs = resolve(titleDetailsObject["genre_id_list"]!!).jsonArray thumbnail_url = result.thumbnailImageUrl ?: result.bannerImageUrl ?: result.thumbnailRectImageUrl
val genreIds = genreIdRefs.map { resolve(it).jsonPrimitive.content.toInt() } if (result.genreIdList.isNotEmpty()) {
genre = genreIds.mapNotNull { genreMap[it] }.joinToString() val genreApiUrl = apiUrl.toHttpUrl().newBuilder()
.addPathSegments("genre/list")
.addQueryParameter("platform", "3")
.addQueryParameter("genre_id_list", result.genreIdList.joinToString(","))
.build()
val genreRequest = hashedGet(genreApiUrl)
val genreResponse = client.newCall(genreRequest).execute()
if (genreResponse.isSuccessful) {
val genreResult = genreResponse.parseAs<GenreListResponse>()
genre = genreResult.genreList.joinToString { it.genreName }
}
}
} }
} }
// Chapters // Chapters
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers) return mangaDetailsRequest(manga)
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val resultIds = response.parseAs<DetailResponse>()
val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() val episodeIds = resultIds.webTitle.episodeIdList.map { it.toString() }
?: throw Exception("Could not find Nuxt data")
val rootArray = nuxtData.parseAs<JsonArray>() if (episodeIds.isEmpty()) {
fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] return emptyList()
}
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 }
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("platform", "3") .add("platform", "3")
@ -284,18 +238,13 @@ class MagazinePocket : HttpSource() {
val result = apiResponse.parseAs<EpisodeListResponse>() val result = apiResponse.parseAs<EpisodeListResponse>()
return result.episodeList.map { chapter -> return result.episodeList
SChapter.create().apply { .map { it.toSChapter(dateFormat) }
val paddedId = chapter.titleId.toString().padStart(5, '0') .reversed()
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)
} override fun getChapterUrl(chapter: SChapter): String {
}.reversed() return baseUrl + chapter.url
} }
// Pages // Pages
@ -381,72 +330,72 @@ class MagazinePocket : HttpSource() {
Pair("(ランキング) ドラマ", "/ranking/27"), Pair("(ランキング) ドラマ", "/ranking/27"),
Pair("(ランキング) ファンタジー", "/ranking/28"), Pair("(ランキング) ファンタジー", "/ranking/28"),
Pair("(ランキング) 日常", "/ranking/29"), Pair("(ランキング) 日常", "/ranking/29"),
Pair("恋愛・ラブコメ", "/search/genre/1"), Pair("恋愛・ラブコメ", "/genre/1"),
Pair("ホラー・ミステリー・サスペンス", "/search/genre/2"), Pair("ホラー・ミステリー・サスペンス", "/genre/2"),
Pair("ギャグ・コメディー・日常", "/search/genre/3"), Pair("ギャグ・コメディー・日常", "/genre/3"),
Pair("SF・ファンタジー", "/search/genre/4"), Pair("SF・ファンタジー", "/genre/4"),
Pair("スポーツ", "/search/genre/5"), Pair("スポーツ", "/genre/5"),
Pair("ヒューマンドラマ", "/search/genre/6"), Pair("ヒューマンドラマ", "/genre/6"),
Pair("裏社会・アングラ・ヤンキー", "/search/genre/7"), Pair("裏社会・アングラ・ヤンキー", "/genre/7"),
Pair("アクション・バトル", "/search/genre/8"), Pair("アクション・バトル", "/genre/8"),
Pair("異世界・異能力", "/search/genre/9"), Pair("異世界・異能力", "/genre/9"),
Pair("読切", "/search/genre/10"), Pair("読切", "/genre/10"),
Pair("MGP", "/search/genre/11"), Pair("MGP", "/genre/11"),
Pair("第98回新人漫画賞", "/search/genre/12"), Pair("第98回新人漫画賞", "/genre/12"),
Pair("第99回新人漫画賞", "/search/genre/13"), Pair("第99回新人漫画賞", "/genre/13"),
Pair("第100回新人漫画賞", "/search/genre/14"), Pair("第100回新人漫画賞", "/genre/14"),
Pair("第101回新人漫画賞", "/search/genre/15"), Pair("第101回新人漫画賞", "/genre/15"),
Pair("第102回新人漫画賞", "/search/genre/16"), Pair("第102回新人漫画賞", "/genre/16"),
Pair("第103回新人漫画賞", "/search/genre/17"), Pair("第103回新人漫画賞", "/genre/17"),
Pair("第104回新人漫画賞", "/search/genre/18"), Pair("第104回新人漫画賞", "/genre/18"),
Pair("第105回新人漫画賞", "/search/genre/19"), Pair("第105回新人漫画賞", "/genre/19"),
Pair("第106回新人漫画賞", "/search/genre/20"), Pair("第106回新人漫画賞", "/genre/20"),
Pair("第107回新人漫画賞", "/search/genre/21"), Pair("第107回新人漫画賞", "/genre/21"),
Pair("第108回新人漫画賞", "/search/genre/22"), Pair("第108回新人漫画賞", "/genre/22"),
Pair("第109回新人漫画賞", "/search/genre/23"), Pair("第109回新人漫画賞", "/genre/23"),
Pair("第110回新人漫画賞", "/search/genre/24"), Pair("第110回新人漫画賞", "/genre/24"),
Pair("2023真夏の読み切り15連弾", "/search/genre/25"), Pair("2023真夏の読み切り15連弾", "/genre/25"),
Pair("マガジンライズ", "/search/genre/26"), Pair("マガジンライズ", "/genre/26"),
Pair("第111回新人漫画賞", "/search/genre/27"), Pair("第111回新人漫画賞", "/genre/27"),
Pair("少女/女性", "/search/genre/28"), Pair("少女/女性", "/genre/28"),
Pair("新人漫画大賞", "/search/genre/29"), Pair("新人漫画大賞", "/genre/29"),
Pair("第75回新人漫画賞", "/search/genre/30"), Pair("第75回新人漫画賞", "/genre/30"),
Pair("第79回新人漫画賞", "/search/genre/31"), Pair("第79回新人漫画賞", "/genre/31"),
Pair("第85回新人漫画賞", "/search/genre/32"), Pair("第85回新人漫画賞", "/genre/32"),
Pair("第88回新人漫画賞", "/search/genre/33"), Pair("第88回新人漫画賞", "/genre/33"),
Pair("第89回新人漫画賞", "/search/genre/34"), Pair("第89回新人漫画賞", "/genre/34"),
Pair("第91回新人漫画賞", "/search/genre/35"), Pair("第91回新人漫画賞", "/genre/35"),
Pair("第92回新人漫画賞", "/search/genre/36"), Pair("第92回新人漫画賞", "/genre/36"),
Pair("第94回新人漫画賞", "/search/genre/37"), Pair("第94回新人漫画賞", "/genre/37"),
Pair("第95回新人漫画賞", "/search/genre/38"), Pair("第95回新人漫画賞", "/genre/38"),
Pair("第96回新人漫画賞", "/search/genre/39"), Pair("第96回新人漫画賞", "/genre/39"),
Pair("第97回新人漫画賞", "/search/genre/40"), Pair("第97回新人漫画賞", "/genre/40"),
Pair("第112回新人漫画賞", "/search/genre/41"), Pair("第112回新人漫画賞", "/genre/41"),
Pair("第113回新人漫画大賞", "/search/genre/42"), Pair("第113回新人漫画大賞", "/genre/42"),
Pair("サッカー", "/search/genre/43"), Pair("サッカー", "/genre/43"),
Pair("テニス", "/search/genre/44"), Pair("テニス", "/genre/44"),
Pair("バスケ", "/search/genre/45"), Pair("バスケ", "/genre/45"),
Pair("格闘技", "/search/genre/46"), Pair("格闘技", "/genre/46"),
Pair("野球", "/search/genre/47"), Pair("野球", "/genre/47"),
Pair("女性向け異世界", "/search/genre/48"), Pair("女性向け異世界", "/genre/48"),
Pair("アニメ化", "/search/genre/49"), Pair("アニメ化", "/genre/49"),
Pair("実写化", "/search/genre/50"), Pair("実写化", "/genre/50"),
Pair("車・バイク", "/search/genre/51"), Pair("車・バイク", "/genre/51"),
Pair("グルメ・料理", "/search/genre/52"), Pair("グルメ・料理", "/genre/52"),
Pair("医療", "/search/genre/53"), Pair("医療", "/genre/53"),
Pair("頭脳戦", "/search/genre/54"), Pair("頭脳戦", "/genre/54"),
Pair("サバイバル", "/search/genre/55"), Pair("サバイバル", "/genre/55"),
Pair("復讐劇", "/search/genre/56"), Pair("復讐劇", "/genre/56"),
Pair("7080年代", "/search/genre/57"), Pair("7080年代", "/genre/57"),
Pair("90年代", "/search/genre/58"), Pair("90年代", "/genre/58"),
Pair("金田一シリーズ", "/search/genre/59"), Pair("金田一シリーズ", "/genre/59"),
Pair("第114回新人漫画大賞", "/search/genre/60"), Pair("第114回新人漫画大賞", "/genre/60"),
Pair("連載獲得ダービー", "/search/genre/61"), Pair("連載獲得ダービー", "/genre/61"),
Pair("有名漫画賞", "/search/genre/62"), Pair("有名漫画賞", "/genre/62"),
Pair("探偵・警察", "/search/genre/63"), Pair("探偵・警察", "/genre/63"),
Pair("歴史・時代", "/search/genre/64"), Pair("歴史・時代", "/genre/64"),
Pair("不倫・浮気", "/search/genre/65"), Pair("不倫・浮気", "/genre/65"),
Pair("犬・猫", "/search/genre/66"), Pair("犬・猫", "/genre/66"),
) )
// Unsupported // Unsupported