ComicMeteor/Kiraboshi: redesign (#11084)

fix and rename to kiraboshi
This commit is contained in:
manti 2025-10-15 14:47:09 +02:00 committed by Draff
parent 145dc251e6
commit 49a970fde8
Signed by: Draff
GPG Key ID: E8A89F3211677653
8 changed files with 176 additions and 97 deletions

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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