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

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

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