@ -1,8 +1,8 @@
|
||||
ext {
|
||||
extName = "Comic Meteor"
|
||||
extName = "Kiraboshi"
|
||||
extClass = ".ComicMeteor"
|
||||
extVersionCode = 2
|
||||
isNsfw = false
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
@ -3,35 +3,40 @@ package eu.kanade.tachiyomi.extension.ja.comicmeteor
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbInterceptor
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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 eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class ComicMeteor : ParsedHttpSource() {
|
||||
|
||||
override val name = "COMICメテオ"
|
||||
|
||||
override val baseUrl = "https://comic-meteor.jp"
|
||||
class ComicMeteor : HttpSource() {
|
||||
|
||||
override val name = "Kiraboshi"
|
||||
override val baseUrl = "https://kirapo.jp"
|
||||
override val lang = "ja"
|
||||
|
||||
override val supportsLatest = false
|
||||
override val versionId = 2
|
||||
|
||||
private val apiUrl = "https://kirapo.jp/api"
|
||||
private val json = Injekt.get<Json>()
|
||||
private val dateFormat = SimpleDateFormat("yyyy年MM月dd日", Locale.JAPAN)
|
||||
|
||||
private var readAtTimestamp: String? = null
|
||||
private var allFiltersList: List<FilterOption> = emptyList()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(SpeedBinbInterceptor(json))
|
||||
@ -47,107 +52,166 @@ class ComicMeteor : ParsedHttpSource() {
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET(
|
||||
"$baseUrl/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&page=$page&get_num=16",
|
||||
headers,
|
||||
)
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return searchMangaRequest(page, "", FilterList())
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = Jsoup.parseBodyFragment(response.body.string(), baseUrl)
|
||||
val manga = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
|
||||
val hasNextPage = manga.size == 16
|
||||
|
||||
return MangasPage(manga, hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = ".update_work_size .update_work_info_img a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
element.selectFirst("img")!!.let {
|
||||
title = it.attr("alt")
|
||||
thumbnail_url = it.absUrl("src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
private lateinit var directory: List<Element>
|
||||
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { searchMangaParse(it) }
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
return searchMangaParse(response)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("comicsearch")
|
||||
.addPathSegment("")
|
||||
.addQueryParameter("search", query)
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
if (query.isNotEmpty()) {
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("word", query)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
val filterSelect = filters.firstInstanceOrNull<AllFilter>()
|
||||
if (filterSelect != null && filterSelect.state != 0 && allFiltersList.isNotEmpty()) {
|
||||
val selection = allFiltersList[filterSelect.state]
|
||||
val urlBuilder = "$baseUrl/titles".toHttpUrl().newBuilder()
|
||||
.addQueryParameter(selection.key, selection.value)
|
||||
return GET(urlBuilder.build(), headers)
|
||||
}
|
||||
return GET("$baseUrl/titles", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.toString().contains("/search")) {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(".content-container .grid-group .w-auto a").map { link ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
val img = link.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("abs:src")
|
||||
}
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
val document = response.asJsoup()
|
||||
|
||||
directory = document.select(searchMangaSelector())
|
||||
return parseDirectory(1)
|
||||
if (allFiltersList.isEmpty()) {
|
||||
readAtTimestamp = document.selectFirst("#more_titles_button")?.attr("data-read-at")
|
||||
|
||||
val filters = mutableListOf<FilterOption>()
|
||||
filters.add(FilterOption("All", "none", "none"))
|
||||
|
||||
document.select("h3:contains(レーベルから選ぶ)").firstOrNull()
|
||||
?.nextElementSibling()?.select("a")?.forEach {
|
||||
filters.add(FilterOption(it.text(), "label", it.attr("href").substringAfter("=")))
|
||||
}
|
||||
|
||||
document.select("h3:contains(ジャンルから選ぶ)").firstOrNull()
|
||||
?.nextElementSibling()?.select("a")?.forEach {
|
||||
filters.add(FilterOption(it.text(), "genre", it.attr("href").substringAfter("=")))
|
||||
}
|
||||
|
||||
document.select("h3:contains(カテゴリから選ぶ)").firstOrNull()
|
||||
?.nextElementSibling()?.select("a")?.forEach {
|
||||
filters.add(FilterOption(it.text(), "category", it.attr("href").substringAfter("=")))
|
||||
}
|
||||
allFiltersList = filters
|
||||
}
|
||||
|
||||
val mangaList = document.select("#titles-container a, .content-container .grid-group .w-auto a").map { link ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
val img = link.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
val readAtTimestamp = document.selectFirst("#more_titles_button")?.attr("data-read-at")
|
||||
if (readAtTimestamp != null) {
|
||||
val apiUrlBuilder = "$apiUrl/title-list".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("read_at", readAtTimestamp)
|
||||
|
||||
response.request.url.queryParameterNames.forEach { param ->
|
||||
if (param != "read_at") {
|
||||
response.request.url.queryParameter(param)?.let { value ->
|
||||
apiUrlBuilder.addQueryParameter(param, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
val apiRequest = GET(apiUrlBuilder.build(), headers)
|
||||
val apiResponse = client.newCall(apiRequest).execute()
|
||||
val apiResult = apiResponse.parseAs<ApiTitlesResponse>()
|
||||
|
||||
val apiMangas = apiResult.data.map { apiTitle ->
|
||||
SManga.create().apply {
|
||||
title = apiTitle.name
|
||||
setUrlWithoutDomain(apiTitle.url)
|
||||
thumbnail_url = apiTitle.thumbnail
|
||||
}
|
||||
}
|
||||
return MangasPage(mangaList + apiMangas, false)
|
||||
}
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
private fun parseDirectory(page: Int): MangasPage {
|
||||
val endRange = minOf(page * 24, directory.size)
|
||||
val manga = directory.subList((page - 1) * 24, endRange).map { searchMangaFromElement(it) }
|
||||
val hasNextPage = endRange < directory.lastIndex
|
||||
|
||||
return MangasPage(manga, hasNextPage)
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
return SManga.create().apply {
|
||||
title = document.selectFirst("main h2")!!.text()
|
||||
thumbnail_url = thumbnail_url
|
||||
author = document.select("a[href*=/authors/]").joinToString(", ") { it.text() }
|
||||
description = document.selectFirst("#plot + div")?.text()
|
||||
genre = document.select("div.pt-5 a.button-gray").joinToString(", ") { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".read_comic_size .read_comic_info_img a"
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val chapters = document.select(".episodes-container .episode-item")
|
||||
.filterNot { it.text().contains("未公開話") }
|
||||
.mapNotNull { item ->
|
||||
item.selectFirst("a")?.let { link ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
name = item.selectFirst(".episode-item-left")!!.text().trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
if (chapters.isNotEmpty()) {
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h2.h2ttl")!!.text()
|
||||
author = document.selectFirst(".work_author_intro_name")
|
||||
?.text()
|
||||
?.substringAfter("著者 :")
|
||||
description = document.selectFirst(".work_story_txt")?.text()
|
||||
genre = document.select(".category_link_box a").joinToString { it.text() }
|
||||
thumbnail_url = document.selectFirst(".latest_info_img img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".work_episode_box .work_episode_table:has(.work_episode_link_orange)"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
name = element.selectFirst(".work_episode_txt")!!.ownText()
|
||||
document.selectFirst(".header-side a.episode-read")?.let { oneshotLink ->
|
||||
val chapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(oneshotLink.attr("href"))
|
||||
name = document.selectFirst(".latest-episode-title")?.text()?.trim()!!
|
||||
val dateStr = document.selectFirst(".last-update")?.text()?.substringBefore("更新")
|
||||
date_upload = dateFormat.tryParse(dateStr)
|
||||
}
|
||||
return listOf(chapter)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private val reader by lazy { SpeedBinbReader(client, headers, json) }
|
||||
|
||||
override fun pageListParse(document: Document) =
|
||||
reader.pageListParse(document)
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return reader.pageListParse(response)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
private class FilterOption(val name: String, val key: String, val value: String)
|
||||
private class AllFilter(options: Array<String>) : Filter.Select<String>("Filter by", options)
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filterList = if (allFiltersList.isEmpty()) {
|
||||
listOf(Filter.Header("Press 'Reset' to attempt to load filters"))
|
||||
} else {
|
||||
listOf(AllFilter(allFiltersList.map { it.name }.toTypedArray()))
|
||||
}
|
||||
return FilterList(filterList)
|
||||
}
|
||||
|
||||
// Unsupported
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.extension.ja.comicmeteor
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ApiTitlesResponse(
|
||||
val data: List<ApiTitle>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ApiTitle(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val thumbnail: String,
|
||||
)
|
||||