diff --git a/lib-multisrc/clipstudioreader/build.gradle.kts b/lib-multisrc/clipstudioreader/build.gradle.kts index 9bdd8fb9ff7..7efc49b234d 100644 --- a/lib-multisrc/clipstudioreader/build.gradle.kts +++ b/lib-multisrc/clipstudioreader/build.gradle.kts @@ -3,6 +3,6 @@ plugins { } keiyoushi { - baseVersionCode = 1 - libVersion = "1.4" + baseVersionCode = 2 + libVersion = "1.6" } diff --git a/lib-multisrc/clipstudioreader/src/eu/kanade/tachiyomi/multisrc/clipstudioreader/ClipStudioReader.kt b/lib-multisrc/clipstudioreader/src/eu/kanade/tachiyomi/multisrc/clipstudioreader/ClipStudioReader.kt index 20dcdb2da9b..e519cdebd02 100644 --- a/lib-multisrc/clipstudioreader/src/eu/kanade/tachiyomi/multisrc/clipstudioreader/ClipStudioReader.kt +++ b/lib-multisrc/clipstudioreader/src/eu/kanade/tachiyomi/multisrc/clipstudioreader/ClipStudioReader.kt @@ -1,53 +1,57 @@ package eu.kanade.tachiyomi.multisrc.clipstudioreader -import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.network.get +import keiyoushi.source.KeiSource import keiyoushi.utils.parseAs +import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import okhttp3.Response import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.jsoup.parser.Parser -abstract class ClipStudioReader : HttpSource() { +abstract class ClipStudioReader : KeiSource() { - override val client = network.client.newBuilder() + override fun OkHttpClient.Builder.configureClient() = this .addInterceptor(Deobfuscator()) .addInterceptor(ImageInterceptor()) - .build() - override fun headersBuilder() = super.headersBuilder() - .set("Referer", "$baseUrl/") + override fun Headers.Builder.configureHeaders() = add("Referer", "$baseUrl/") - override fun pageListParse(response: Response): List { + abstract suspend fun getViewerResponse(chapter: SChapter): Response + + override suspend fun getPageList(chapter: SChapter): List { + val response = getViewerResponse(chapter) val requestUrl = response.request.url val contentId = requestUrl.queryParameter("c") if (contentId != null) { // EPUB-based path val tokenUrl = "$baseUrl/api/tokens/viewer?content_id=$contentId".toHttpUrl() - val tokenResponse = client.newCall(GET(tokenUrl, headers)).execute() + val tokenResponse = client.get(tokenUrl, headers) val viewerToken = tokenResponse.parseAs().token val metaUrl = "$baseUrl/api/contents/$contentId/meta".toHttpUrl() - val apiHeaders = headersBuilder().add("Authorization", "Bearer $viewerToken").build() - val metaResponse = client.newCall(GET(metaUrl, apiHeaders)).execute() + val apiHeaders = headers.newBuilder().add("Authorization", "Bearer $viewerToken").build() + val metaResponse = client.get(metaUrl, apiHeaders) val contentBaseUrl = metaResponse.parseAs().content.baseUrl val preprocessUrl = "$contentBaseUrl/preprocess-settings.json" - val obfuscationResponse = client.newCall(GET(preprocessUrl, headers)).execute() + val obfuscationResponse = client.get(preprocessUrl, headers) val obfuscationKey = obfuscationResponse.parseAs().obfuscateImageKey val containerUrl = "$contentBaseUrl/META-INF/container.xml" - val containerResponse = client.newCall(GET(containerUrl, headers)).execute() + val containerResponse = client.get(containerUrl, headers) val containerDoc = Jsoup.parse(containerResponse.body.string(), containerUrl, Parser.xmlParser()) val opfPath = containerDoc.selectFirst("*|rootfile")?.attr("full-path") ?: throw Exception("Failed to find rootfile in container.xml") val opfUrl = (contentBaseUrl.removeSuffix("/") + "/" + opfPath).toHttpUrl() - val opfResponse = client.newCall(GET(opfUrl, headers)).execute() + val opfResponse = client.get(opfUrl, headers) val opfDoc = opfResponse.asJsoup() val imageManifestItems = opfDoc.select("*|item[media-type^=image/]") @@ -89,7 +93,7 @@ abstract class ClipStudioReader : HttpSource() { addQueryParameter("param", authkey) }.build() - val faceResponse = client.newCall(GET(faceUrl, headers)).execute() + val faceResponse = client.get(faceUrl, headers) if (!faceResponse.isSuccessful) throw Exception("HTTP error ${faceResponse.code} while fetching face.xml") val faceData = faceResponse.use { parseFaceData(it.asJsoup()) } @@ -106,7 +110,8 @@ abstract class ClipStudioReader : HttpSource() { } } - override fun imageUrlParse(response: Response): String { + override suspend fun getImageUrl(page: Page): String { + val response = client.get(page.url, headers) val requestUrl = response.request.url val document = response.asJsoup() diff --git a/src/ja/comicfesta/build.gradle.kts b/src/ja/comicfesta/build.gradle.kts index 2874ac6edb4..83ad9196f4d 100644 --- a/src/ja/comicfesta/build.gradle.kts +++ b/src/ja/comicfesta/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "Comic Festa" versionCode = 1 contentWarning = ContentWarning.MIXED - libVersion = "1.4" + libVersion = "1.6" theme = "clipstudioreader" source { diff --git a/src/ja/comicfesta/src/eu/kanade/tachiyomi/extension/ja/comicfesta/ComicFesta.kt b/src/ja/comicfesta/src/eu/kanade/tachiyomi/extension/ja/comicfesta/ComicFesta.kt index b29972f7aa0..b4c830b6956 100644 --- a/src/ja/comicfesta/src/eu/kanade/tachiyomi/extension/ja/comicfesta/ComicFesta.kt +++ b/src/ja/comicfesta/src/eu/kanade/tachiyomi/extension/ja/comicfesta/ComicFesta.kt @@ -3,19 +3,23 @@ package eu.kanade.tachiyomi.extension.ja.comicfesta import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.multisrc.clipstudioreader.ClipStudioReader -import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.multisrc.clipstudioreader.Deobfuscator +import eu.kanade.tachiyomi.multisrc.clipstudioreader.ImageInterceptor import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.SMangaUpdate import eu.kanade.tachiyomi.util.asJsoup import keiyoushi.annotation.Source import keiyoushi.lib.cookieinterceptor.CookieInterceptor +import keiyoushi.network.get import keiyoushi.utils.extractNextJs import keiyoushi.utils.getPreferencesLazy import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request +import okhttp3.Interceptor +import okhttp3.OkHttpClient import okhttp3.Response import java.io.IOException @@ -24,62 +28,65 @@ abstract class ComicFesta : ClipStudioReader(), ConfigurableSource { - override val supportsLatest = true - private val domain = baseUrl.toHttpUrl().host private val preferences by getPreferencesLazy() private val rscHeaders = headersBuilder() .add("rsc", "1") .build() - override val client = super.client.newBuilder() - .addNetworkInterceptor(CookieInterceptor(domain, listOf("checked_age" to "1", "sp_display" to "1", "cf_checked_age_guest" to "1", "cf_checked_age" to "1"))) - .addInterceptor { - val request = it.request() - val response = it.proceed(request) - if (request.url.pathSegments.first() == "volumes" && (response.request.url.pathSegments.first() == "entry" || response.request.url.pathSegments.first() == "error")) { - throw IOException("Log in via WebView and purchase this product to read.") - } - response - } - .build() + override fun OkHttpClient.Builder.configureClient() = apply { + addInterceptor(Deobfuscator()) + addInterceptor(ImageInterceptor()) + addNetworkInterceptor(CookieInterceptor(domain, listOf("checked_age" to "1", "sp_display" to "1", "cf_checked_age_guest" to "1", "cf_checked_age" to "1"))) + addInterceptor( + Interceptor { chain -> + val request = chain.request() + val response = chain.proceed(request) + if (request.url.pathSegments.first() == "volumes" && (response.request.url.pathSegments.first() == "entry" || response.request.url.pathSegments.first() == "error")) { + throw IOException("Log in via WebView and purchase this product to read.") + } + response + }, + ) + } - override fun popularMangaRequest(page: Int): Request { + override fun getMangaUrl(manga: SManga): String = "$baseUrl/titles/${manga.url}" + + override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/volumes/${chapter.url}" + + override suspend fun getPopularManga(page: Int): MangasPage { val url = "$baseUrl/sales_rankings/monthly_general".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString()) .build() - return GET(url, rscHeaders) - } - - override fun popularMangaParse(response: Response): MangasPage { + val response = client.get(url, rscHeaders) val result = response.extractNextJs() val mangas = result?.titles.orEmpty().map { it.toSManga() } val hasNextPage = response.request.url.queryParameter("page") != "2" return MangasPage(mangas, hasNextPage) } - override fun latestUpdatesRequest(page: Int): Request { + override suspend fun getLatestUpdates(page: Int): MangasPage { val url = "$baseUrl/titles".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString()) .addQueryParameter("sort", "release") .addQueryParameter("search_form[other_item][]", "new") .build() - return GET(url, headers) + val response = client.get(url, headers) + return parseSearchResponse(response) } - override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val url = "$baseUrl/titles".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString()) .addQueryParameter("search_form[keyword]", query) .addQueryParameter("search", query) .addQueryParameter("commit", "search") .build() - return GET(url, headers) + val response = client.get(url, headers) + return parseSearchResponse(response) } - override fun searchMangaParse(response: Response): MangasPage { + private fun parseSearchResponse(response: Response): MangasPage { val document = response.asJsoup() val mangas = document.select("div.list-detail-box").map { SManga.create().apply { @@ -92,30 +99,41 @@ abstract class ComicFesta : return MangasPage(mangas, hasNextPage) } - override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl/titles/${manga.url}", headers) - - override fun mangaDetailsParse(response: Response): SManga { + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { + val response = client.get("$baseUrl/titles/${manga.url}", headers) val document = response.asJsoup() - return SManga.create().apply { - title = document.selectFirst("h1[class*='titleName']")!!.text() - author = document.select("a[href*='/authors/']").joinToString { it.text() } - description = document.selectFirst("div[class*='description']")?.text() - genre = document.select("[class*='outlineTag'] a").joinToString { it.text() } - thumbnail_url = document.selectFirst("img[class*='thumbnail']")?.absUrl("src") - val statusText = document.select("[class*='latest-package-num-display']").text() - status = if (statusText.contains("完結")) SManga.COMPLETED else SManga.ONGOING + val hideLocked = preferences.getBoolean(HIDE_LOCKED_PREF_KEY, false) + + val sManga = if (fetchDetails) { + SManga.create().apply { + title = document.selectFirst("h1[class*='titleName']")!!.text() + author = document.select("a[href*='/authors/']").joinToString { it.text() } + description = document.selectFirst("div[class*='description']")?.text() + genre = document.select("[class*='outlineTag'] a").joinToString { it.text() } + thumbnail_url = document.selectFirst("img[class*='thumbnail']")?.absUrl("src") + val statusText = document.select("[class*='latest-package-num-display']").text() + status = if (statusText.contains("完結")) SManga.COMPLETED else SManga.ONGOING + } + } else { + manga } - } - override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) // no redirect for r18 content with rsc + val sChapters = if (fetchChapters) { + val result = document.extractNextJs() + result?.toSChapterList(hideLocked).orEmpty().reversed() + } else { + chapters + } - override fun chapterListParse(response: Response): List { - val hideLocked = preferences.getBoolean(HIDE_LOCKED_PREF_KEY, false) - val result = response.extractNextJs() - return result?.toSChapterList(hideLocked).orEmpty().reversed() + return SMangaUpdate(sManga, sChapters) } - override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/volumes/${chapter.url}", headers) + override suspend fun getViewerResponse(chapter: SChapter): Response = client.get("$baseUrl/volumes/${chapter.url}", headers) override fun setupPreferenceScreen(screen: PreferenceScreen) { SwitchPreferenceCompat(screen.context).apply { diff --git a/src/ja/firecross/build.gradle.kts b/src/ja/firecross/build.gradle.kts index e22877cd388..25978364e49 100644 --- a/src/ja/firecross/build.gradle.kts +++ b/src/ja/firecross/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "FireCross" versionCode = 2 contentWarning = ContentWarning.SAFE - libVersion = "1.4" + libVersion = "1.6" theme = "clipstudioreader" source { diff --git a/src/ja/firecross/src/eu/kanade/tachiyomi/extension/ja/firecross/FireCross.kt b/src/ja/firecross/src/eu/kanade/tachiyomi/extension/ja/firecross/FireCross.kt index 8b55c974351..adde41493e5 100644 --- a/src/ja/firecross/src/eu/kanade/tachiyomi/extension/ja/firecross/FireCross.kt +++ b/src/ja/firecross/src/eu/kanade/tachiyomi/extension/ja/firecross/FireCross.kt @@ -4,29 +4,27 @@ import android.content.SharedPreferences import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.multisrc.clipstudioreader.ClipStudioReader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.source.ConfigurableSource 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.model.SMangaUpdate import eu.kanade.tachiyomi.util.asJsoup import keiyoushi.annotation.Source +import keiyoushi.network.get +import keiyoushi.network.post import keiyoushi.utils.firstInstance import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.parseAs import keiyoushi.utils.toJsonString -import keiyoushi.utils.tryParse import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request import okhttp3.Response -import rx.Observable -import java.text.SimpleDateFormat +import java.time.LocalDate +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.Locale @Source @@ -37,12 +35,11 @@ abstract class FireCross : override val supportsLatest = false private val apiUrl = "$baseUrl/api" - private val dateFormat = SimpleDateFormat("yyyy/M/d", Locale.ROOT) + private val dateFormat = DateTimeFormatter.ofPattern("yyyy/M/d", Locale.ROOT) private val preferences: SharedPreferences by getPreferencesLazy() - override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ebook/comics?sort=1&page=$page", headers) - - override fun popularMangaParse(response: Response): MangasPage { + override suspend fun getPopularManga(page: Int): MangasPage { + val response = client.get("$baseUrl/ebook/comics?sort=1&page=$page", headers) val document = response.asJsoup() val mangas = document.select("ul.seriesList li.seriesList_item").map { SManga.create().apply { @@ -56,7 +53,7 @@ abstract class FireCross : return MangasPage(mangas, hasNextPage) } - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val url = "$baseUrl/search".toHttpUrl().newBuilder().apply { addQueryParameter("q", query) addQueryParameter("t", "1") @@ -69,10 +66,8 @@ abstract class FireCross : } } }.build() - return GET(url, headers) - } + val response = client.get(url, headers) - override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() val mangas = document.select("ul.seriesList#search-result li.seriesList_item").map { element -> SManga.create().apply { @@ -86,19 +81,15 @@ abstract class FireCross : return MangasPage(mangas, hasNextPage) } - override fun mangaDetailsParse(response: Response): SManga { - val document = response.asJsoup() - return SManga.create().apply { - title = document.selectFirst("h1.ebook-series-title")!!.text() - author = document.select("ul.ebook-series-author li").joinToString { it.text() } - description = document.selectFirst("p.ebook-series-synopsis")?.text() - genre = document.select("div.book-genre a").joinToString { it.text() } - } - } - - override fun fetchChapterList(manga: SManga): Observable> { + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { val hideLocked = preferences.getBoolean(HIDE_LOCKED_PREF_KEY, false) - val chapters = mutableListOf() + var sManga = manga + val sChapters = mutableListOf() var page = 1 while (true) { @@ -106,46 +97,59 @@ abstract class FireCross : .addQueryParameter("sort", "latest") .addQueryParameter("page", page.toString()) .build() - val document = client.newCall(GET(url, headers)).execute().asJsoup() - - chapters += document.select("div.shop-item--episode").mapNotNull { - val info = it.selectFirst(".shop-item-info")!! - val nameText = info.selectFirst("span.shop-item-info-name")?.text()!! - val dateText = info.selectFirst("span.shop-item-info-release")?.text()?.substringAfter("公開:") - val form = it.selectFirst("form[data-api=reader]") - - SChapter.create().apply { - name = nameText - date_upload = dateFormat.tryParse(dateText) - - when { - form != null -> { - val token = form.selectFirst("input[name=_token]")!!.attr("value") - val ebookId = form.selectFirst("input[name=ebook_id]")!!.attr("value") - this.url = ChapterId(token, ebookId).toJsonString() - } + val document = client.get(url, headers).asJsoup() + + if (fetchDetails && page == 1) { + sManga = SManga.create().apply { + title = document.selectFirst("h1.ebook-series-title")!!.text() + author = document.select("ul.ebook-series-author li").joinToString { it.text() } + description = document.selectFirst("p.ebook-series-synopsis")?.text() + genre = document.select("div.book-genre a").joinToString { it.text() } + } + } - else -> { - if (hideLocked) return@mapNotNull null - name = "🔒 $nameText" - val rentalId = it.attr("data-id") - this.url = "rental/$rentalId" + if (fetchChapters) { + sChapters += document.select("div.shop-item--episode").mapNotNull { + val info = it.selectFirst(".shop-item-info")!! + val nameText = info.selectFirst("span.shop-item-info-name")?.text()!! + val dateText = info.selectFirst("span.shop-item-info-release")?.text()?.substringAfter("公開:") + val form = it.selectFirst("form[data-api=reader]") + + SChapter.create().apply { + name = nameText + date_upload = runCatching { + LocalDate.parse(dateText, dateFormat).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() + }.getOrDefault(0L) + + when { + form != null -> { + val token = form.selectFirst("input[name=_token]")!!.attr("value") + val ebookId = form.selectFirst("input[name=ebook_id]")!!.attr("value") + this.url = ChapterId(token, ebookId).toJsonString() + } + + else -> { + if (hideLocked) return@mapNotNull null + name = "🔒 $nameText" + val rentalId = it.attr("data-id") + this.url = "rental/$rentalId" + } } } } } val hasNextPage = document.selectFirst("li.ebookSeries_paginationLink.active ~ li.ebookSeries_paginationLink") != null - if (!hasNextPage) break + if (!hasNextPage || !fetchChapters) break page++ } - return Observable.just(chapters) + return SMangaUpdate(sManga, if (fetchChapters) sChapters else chapters) } - override fun fetchPageList(chapter: SChapter): Observable> { + override suspend fun getViewerResponse(chapter: SChapter): Response { if (!chapter.url.startsWith("{")) { - return Observable.error(Exception("Log in via WebView and purchase this chapter to read.")) + throw Exception("Log in via WebView and purchase this chapter to read.") } val chapterId = chapter.url.parseAs() @@ -155,21 +159,15 @@ abstract class FireCross : .add("ebook_id", chapterId.id) .build() - val apiHeaders = headersBuilder() + val apiHeaders = headers.newBuilder() .add("X-Requested-With", "XMLHttpRequest") .build() - val apiRequest = POST("$apiUrl/reader", apiHeaders, formBody) - - return client.newCall(apiRequest).asObservable().map { - val redirectUrl = it.parseAs().redirect - val viewerRequest = GET(redirectUrl, headers) - val viewerResponse = client.newCall(viewerRequest).execute() - super.pageListParse(viewerResponse) - } + val redirectUrl = client.post("$apiUrl/reader", apiHeaders, formBody).parseAs().redirect + return client.get(redirectUrl, headers) } - override fun getFilterList(): FilterList = FilterList( + override fun getFilterList(data: kotlinx.serialization.json.JsonElement?): FilterList = FilterList( Filter.Header("Note: Search and active filters are applied together"), Filter.Header("Note: Novels only show images, not text!"), LabelFilter(), @@ -187,9 +185,5 @@ abstract class FireCross : private const val HIDE_LOCKED_PREF_KEY = "hide_locked" } - // Unsupported - override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() - override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() - override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException() - override fun chapterListParse(response: Response): List = throw UnsupportedOperationException() + override suspend fun getLatestUpdates(page: Int): MangasPage = throw UnsupportedOperationException() }