diff --git a/src/ja/comicmeteor/build.gradle b/src/ja/comicmeteor/build.gradle index 4512f2a5e..b660e0234 100644 --- a/src/ja/comicmeteor/build.gradle +++ b/src/ja/comicmeteor/build.gradle @@ -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" diff --git a/src/ja/comicmeteor/res/mipmap-hdpi/ic_launcher.png b/src/ja/comicmeteor/res/mipmap-hdpi/ic_launcher.png index 185966c0a..e427a0709 100644 Binary files a/src/ja/comicmeteor/res/mipmap-hdpi/ic_launcher.png and b/src/ja/comicmeteor/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/comicmeteor/res/mipmap-mdpi/ic_launcher.png b/src/ja/comicmeteor/res/mipmap-mdpi/ic_launcher.png index 994b793c4..87d570282 100644 Binary files a/src/ja/comicmeteor/res/mipmap-mdpi/ic_launcher.png and b/src/ja/comicmeteor/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/comicmeteor/res/mipmap-xhdpi/ic_launcher.png b/src/ja/comicmeteor/res/mipmap-xhdpi/ic_launcher.png index f87997f8b..773e99e7a 100644 Binary files a/src/ja/comicmeteor/res/mipmap-xhdpi/ic_launcher.png and b/src/ja/comicmeteor/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/comicmeteor/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/comicmeteor/res/mipmap-xxhdpi/ic_launcher.png index 2e47a78df..b102f9df4 100644 Binary files a/src/ja/comicmeteor/res/mipmap-xxhdpi/ic_launcher.png and b/src/ja/comicmeteor/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/comicmeteor/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/comicmeteor/res/mipmap-xxxhdpi/ic_launcher.png index 349bf7bd2..f05f0ac1b 100644 Binary files a/src/ja/comicmeteor/res/mipmap-xxxhdpi/ic_launcher.png and b/src/ja/comicmeteor/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/ComicMeteor.kt b/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/ComicMeteor.kt index 89ef2c5e8..37a846d33 100644 --- a/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/ComicMeteor.kt +++ b/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/ComicMeteor.kt @@ -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() + private val dateFormat = SimpleDateFormat("yyyy年MM月dd日", Locale.JAPAN) + + private var readAtTimestamp: String? = null + private var allFiltersList: List = 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 - - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList, - ): Observable { - 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() + 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() + 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() + + 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 { + 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 { + 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) : Filter.Select("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() } diff --git a/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/Dto.kt b/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/Dto.kt new file mode 100644 index 000000000..11a64c546 --- /dev/null +++ b/src/ja/comicmeteor/src/eu/kanade/tachiyomi/extension/ja/comicmeteor/Dto.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.ja.comicmeteor + +import kotlinx.serialization.Serializable + +@Serializable +class ApiTitlesResponse( + val data: List, +) + +@Serializable +class ApiTitle( + val name: String, + val url: String, + val thumbnail: String, +)