Fix MagazinePocket (#11266)

* fix MagazinePocket

* Update src/ja/magazinepocket/src/eu/kanade/tachiyomi/extension/ja/magazinepocket/MagazinePocket.kt
This commit is contained in:
manti 2025-10-27 13:12:50 +01:00 committed by Draff
parent 546696c0d2
commit cf7446489d
Signed by: Draff
GPG Key ID: E8A89F3211677653
9 changed files with 610 additions and 27 deletions

View File

@ -1,9 +1,7 @@
ext {
extName = 'Magazine Pocket'
extClass = '.MagazinePocket'
themePkg = 'gigaviewer'
baseUrl = 'https://pocket.shonenmagazine.com'
overrideVersionCode = 0
extVersionCode = 9
isNsfw = false
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.extension.ja.magazinepocket
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class RankingApiResponse(
@SerialName("ranking_title_list") val rankingTitleList: List<RankingTitleId>,
)
@Serializable
class RankingTitleId(
val id: Int,
)
@Serializable
class TitleListResponse(
@SerialName("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,
)
@Serializable
class EpisodeListResponse(
@SerialName("episode_list") val episodeList: List<Episode>,
)
@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,
)
@Serializable
class ViewerApiResponse(
@SerialName("page_list") val pageList: List<String>,
@SerialName("scramble_seed") val scrambleSeed: Long,
)
@Serializable
class SearchApiResponse(
@SerialName("search_title_list") val searchTitleList: List<SearchTitleDetail>,
)
@Serializable
class SearchTitleDetail(
@SerialName("title_id") val titleId: Int,
@SerialName("title_name") val titleName: String,
@SerialName("banner_image_url") val bannerImageUrl: String? = null,
)

View File

@ -0,0 +1,95 @@
package eu.kanade.tachiyomi.extension.ja.magazinepocket
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class ImageInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val fragment = request.url.fragment
if (fragment.isNullOrEmpty() || !fragment.startsWith("scramble_seed=")) {
return chain.proceed(request)
}
val seed = fragment.substringAfter("scramble_seed=").toLong()
val response = chain.proceed(request)
val descrambledBody = descrambleImage(response.body, seed)
return response.newBuilder().body(descrambledBody).build()
}
private class Coord(val x: Int, val y: Int)
private class CoordPair(val source: Coord, val dest: Coord)
private fun xorshift32(seed: UInt): UInt {
var n = seed
n = n xor (n shl 13)
n = n xor (n shr 17)
n = n xor (n shl 5)
return n
}
private fun getUnscrambledCoords(seed: Long): List<CoordPair> {
var seed32 = seed.toUInt()
val pairs = mutableListOf<Pair<UInt, Int>>()
for (i in 0 until 16) {
seed32 = xorshift32(seed32)
pairs.add(seed32 to i)
}
pairs.sortBy { it.first }
val sortedVal = pairs.map { it.second }
return sortedVal.mapIndexed { i, e ->
CoordPair(
source = Coord(x = e % 4, y = e / 4),
dest = Coord(x = i % 4, y = i / 4),
)
}
}
private fun descrambleImage(responseBody: ResponseBody, seed: Long): 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
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)
unscrambledCoords.forEach { coord ->
val sx = coord.source.x * tileWidth
val sy = coord.source.y * tileHeight
val dx = coord.dest.x * tileWidth
val dy = coord.dest.y * tileHeight
val srcRect = Rect(sx, sy, sx + tileWidth, sy + tileHeight)
val destRect = Rect(dx, dy, dx + tileWidth, dy + tileHeight)
canvas.drawBitmap(originalBitmap, srcRect, destRect, null)
}
originalBitmap.recycle()
val outputStream = ByteArrayOutputStream()
descrambledBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
descrambledBitmap.recycle()
return outputStream.toByteArray().toResponseBody("image/jpeg".toMediaType())
}
}

View File

@ -1,36 +1,454 @@
package eu.kanade.tachiyomi.extension.ja.magazinepocket
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.OkHttpClient
import org.jsoup.nodes.Element
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
import okhttp3.Request
import okhttp3.Response
import okio.ByteString.Companion.encodeUtf8
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale
import java.util.TimeZone
class MagazinePocket : GigaViewer(
"Magazine Pocket",
"https://pocket.shonenmagazine.com",
"ja",
"https://cdn-img.pocket.shonenmagazine.com/public/page",
) {
class MagazinePocket : HttpSource() {
override val name = "Magazine Pocket"
override val baseUrl = "https://pocket.shonenmagazine.com"
override val lang = "ja"
override val supportsLatest = true
override val versionId = 2
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(::imageIntercept)
private val apiUrl = "https://api.pocket.shonenmagazine.com"
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.JAPAN)
private val pageLimit = 25
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(ImageInterceptor())
.build()
override val publisher: String = "講談社"
override fun popularMangaSelector(): String = "ul.daily-series li.daily-series-item > a"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.selectFirst("h4.daily-series-title")!!.text()
thumbnail_url = element.selectFirst("div.daily-series-thumb img")!!.attr("data-src")
setUrlWithoutDomain(element.attr("href")!!)
// Popular
override fun popularMangaRequest(page: Int): Request {
val offset = (page - 1) * pageLimit
val url = apiUrl.toHttpUrl().newBuilder()
.addPathSegments("ranking/all")
.addQueryParameter("platform", "3")
.addQueryParameter("ranking_id", "30")
.addQueryParameter("offset", offset.toString())
.addQueryParameter("limit", "26")
.build()
return hashedGet(url)
}
override fun latestUpdatesSelector(): String = "section.daily.$dayOfWeek " + popularMangaSelector()
override fun popularMangaParse(response: Response): MangasPage {
val rankingResult = response.parseAs<RankingApiResponse>()
val titleIds = rankingResult.rankingTitleList.map { it.id.toString().padStart(5, '0') }
if (titleIds.isEmpty()) {
return MangasPage(emptyList(), false)
}
override fun getCollections(): List<Collection> = listOf(
Collection("マガポケ連載一覧", ""),
Collection("週刊少年マガジン連載一覧", "smaga"),
Collection("別冊少年マガジン連載一覧", "bmaga"),
val hasNextPage = titleIds.size > pageLimit
val mangaIdsToFetch = if (hasNextPage) titleIds.dropLast(1) else titleIds
val detailsUrl = apiUrl.toHttpUrl().newBuilder()
.addPathSegments("title/list")
.addQueryParameter("platform", "3")
.addQueryParameter("title_id_list", mangaIdsToFetch.joinToString(","))
.build()
val detailsRequest = hashedGet(detailsUrl)
val detailsResponse = client.newCall(detailsRequest).execute()
if (!detailsResponse.isSuccessful) {
throw Exception("Failed to fetch title details: ${detailsResponse.code} - ${detailsResponse.body.string()}")
}
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
}
}
return MangasPage(mangas, hasNextPage)
}
// Latest
override fun latestUpdatesRequest(page: Int): Request {
val calendar = GregorianCalendar(TimeZone.getTimeZone("Asia/Tokyo")).apply {
time = Date()
add(Calendar.DAY_OF_MONTH, -(page - 1))
}
val dateString = buildString {
append(calendar.get(Calendar.YEAR))
append('-')
append((calendar.get(Calendar.MONTH) + 1).toString().padStart(2, '0'))
append('-')
append(calendar.get(Calendar.DAY_OF_MONTH).toString().padStart(2, '0'))
}
val url = apiUrl.toHttpUrl().newBuilder()
.addPathSegments("web/top/updated/title")
.addQueryParameter("base_date", dateString)
.addQueryParameter("platform", "3")
.build()
return hashedGet(url)
}
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)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val url = apiUrl.toHttpUrl().newBuilder()
.addPathSegments("web/search/title")
.addQueryParameter("keyword", query)
.addQueryParameter("limit", "99999")
.addQueryParameter("platform", "3")
.build()
return hashedGet(url)
}
val genreFilter = filters.firstInstance<GenreFilter>()
val uriPart = genreFilter.toUriPart()
if (uriPart.startsWith("/search/genre/")) {
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegments(uriPart.removePrefix("/"))
.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 }
}
}
return MangasPage(mangas, false)
}
return popularMangaParse(response)
}
// Details
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>()
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 {
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)
}
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 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 }
val formBody = FormBody.Builder()
.add("platform", "3")
.add("episode_id_list", episodeIds.joinToString(","))
.build()
val params = (0 until formBody.size).associate { formBody.name(it) to formBody.value(it) }
val hash = generateHash(params)
val postHeaders = headersBuilder()
.add("Origin", baseUrl)
.add("x-manga-is-crawler", "false")
.add("x-manga-hash", hash)
.build()
val apiRequest = POST("$apiUrl/episode/list", postHeaders, formBody)
val apiResponse = client.newCall(apiRequest).execute()
if (!apiResponse.isSuccessful) {
throw Exception("API request failed with code ${apiResponse.code}: ${apiResponse.body.string()}")
}
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()
}
// Pages
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservable()
.map { response ->
if (!response.isSuccessful) {
if (response.code == 400) {
throw Exception("This chapter is locked. Log in via WebView and rent or purchase this chapter to read.")
}
throw Exception("HTTP error ${response.code}")
}
pageListParse(response)
}
}
override fun pageListParse(response: Response): List<Page> {
val apiResponse = response.parseAs<ViewerApiResponse>()
val seed = apiResponse.scrambleSeed
return apiResponse.pageList.mapIndexed { index, imageUrl ->
Page(index, imageUrl = "$imageUrl#scramble_seed=$seed")
}
}
override fun pageListRequest(chapter: SChapter): Request {
val episodeId = chapter.url.substringAfter("episode/")
val url = "$apiUrl/web/episode/viewer".toHttpUrl().newBuilder()
.addQueryParameter("platform", "3")
.addQueryParameter("episode_id", episodeId)
.build()
return hashedGet(url)
}
private fun generateHash(params: Map<String, String>, birthday: String = "", expires: String = ""): String {
val paramStrings = params.toSortedMap().map { (key, value) ->
getHashedParam(key, value)
}
val joinedParams = paramStrings.joinToString(",")
val hash1 = joinedParams.encodeUtf8().sha256().hex()
val cookieHash = getHashedParam(birthday, expires)
val finalString = "$hash1$cookieHash"
return finalString.encodeUtf8().sha512().hex()
}
private fun getHashedParam(key: String, value: String): String {
val keyHash = key.encodeUtf8().sha256().hex()
val valueHash = value.encodeUtf8().sha512().hex()
return "${keyHash}_$valueHash"
}
private fun hashedGet(url: HttpUrl): Request {
val queryParams = url.queryParameterNames.associateWith { url.queryParameter(it)!! }
val hash = generateHash(queryParams)
val newHeaders = headersBuilder()
.add("x-manga-hash", hash)
.build()
return GET(url, newHeaders)
}
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Search query will ignore genre filter"),
GenreFilter(getGenreList()),
)
private class GenreFilter(private val genres: Array<Pair<String, String>>) :
Filter.Select<String>("Filter by", genres.map { it.first }.toTypedArray()) {
fun toUriPart() = genres[state].second
}
private fun getGenreList() = arrayOf(
Pair("(ランキング) すべて", "/ranking/30"),
Pair("(ランキング) まずはコレ!", "/ranking/2"),
Pair("(ランキング) オリジナル", "/ranking/1"),
Pair("(ランキング) 新作", "/ranking/31"),
Pair("(ランキング) アクション", "/ranking/21"),
Pair("(ランキング) スポーツ", "/ranking/22"),
Pair("(ランキング) 恋愛", "/ranking/23"),
Pair("(ランキング) 異世界", "/ranking/24"),
Pair("(ランキング) サスペンス", "/ranking/25"),
Pair("(ランキング) 裏社会・ヤンキー", "/ranking/26"),
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"),
)
// Unsupported
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
}