@ -1,8 +1,8 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = "Comic Meteor"
|
extName = "Kiraboshi"
|
||||||
extClass = ".ComicMeteor"
|
extClass = ".ComicMeteor"
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
isNsfw = false
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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.SpeedBinbInterceptor
|
||||||
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbReader
|
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbReader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.firstInstanceOrNull
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class ComicMeteor : ParsedHttpSource() {
|
class ComicMeteor : HttpSource() {
|
||||||
|
|
||||||
override val name = "COMICメテオ"
|
|
||||||
|
|
||||||
override val baseUrl = "https://comic-meteor.jp"
|
|
||||||
|
|
||||||
|
override val name = "Kiraboshi"
|
||||||
|
override val baseUrl = "https://kirapo.jp"
|
||||||
override val lang = "ja"
|
override val lang = "ja"
|
||||||
|
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
private val apiUrl = "https://kirapo.jp/api"
|
||||||
private val json = Injekt.get<Json>()
|
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()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(SpeedBinbInterceptor(json))
|
.addInterceptor(SpeedBinbInterceptor(json))
|
||||||
@ -47,107 +52,166 @@ class ComicMeteor : ParsedHttpSource() {
|
|||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET(
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
"$baseUrl/wp-admin/admin-ajax.php?action=get_flex_titles_for_toppage&page=$page&get_num=16",
|
return searchMangaRequest(page, "", FilterList())
|
||||||
headers,
|
}
|
||||||
)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val document = Jsoup.parseBodyFragment(response.body.string(), baseUrl)
|
return searchMangaParse(response)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder()
|
if (query.isNotEmpty()) {
|
||||||
.addPathSegment("comicsearch")
|
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||||
.addPathSegment("")
|
.addQueryParameter("word", query)
|
||||||
.addQueryParameter("search", query)
|
.build()
|
||||||
.build()
|
return GET(url, headers)
|
||||||
|
}
|
||||||
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 {
|
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()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
directory = document.select(searchMangaSelector())
|
if (allFiltersList.isEmpty()) {
|
||||||
return parseDirectory(1)
|
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 {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val endRange = minOf(page * 24, directory.size)
|
val document = response.asJsoup()
|
||||||
val manga = directory.subList((page - 1) * 24, endRange).map { searchMangaFromElement(it) }
|
return SManga.create().apply {
|
||||||
val hasNextPage = endRange < directory.lastIndex
|
title = document.selectFirst("main h2")!!.text()
|
||||||
|
thumbnail_url = thumbnail_url
|
||||||
return MangasPage(manga, hasNextPage)
|
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()
|
document.selectFirst(".header-side a.episode-read")?.let { oneshotLink ->
|
||||||
|
val chapter = SChapter.create().apply {
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
setUrlWithoutDomain(oneshotLink.attr("href"))
|
||||||
title = document.selectFirst("h2.h2ttl")!!.text()
|
name = document.selectFirst(".latest-episode-title")?.text()?.trim()!!
|
||||||
author = document.selectFirst(".work_author_intro_name")
|
val dateStr = document.selectFirst(".last-update")?.text()?.substringBefore("更新")
|
||||||
?.text()
|
date_upload = dateFormat.tryParse(dateStr)
|
||||||
?.substringAfter("著者 :")
|
}
|
||||||
description = document.selectFirst(".work_story_txt")?.text()
|
return listOf(chapter)
|
||||||
genre = document.select(".category_link_box a").joinToString { it.text() }
|
}
|
||||||
thumbnail_url = document.selectFirst(".latest_info_img img")?.absUrl("src")
|
return emptyList()
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val reader by lazy { SpeedBinbReader(client, headers, json) }
|
private val reader by lazy { SpeedBinbReader(client, headers, json) }
|
||||||
|
|
||||||
override fun pageListParse(document: Document) =
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
reader.pageListParse(document)
|
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,
|
||||||
|
)
|
||||||