-
Notifications
You must be signed in to change notification settings - Fork 1.6k
ClipStudioReader: extlib 1.6 #18134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
ClipStudioReader: extlib 1.6 #18134
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,6 @@ plugins { | |
| } | ||
|
|
||
| keiyoushi { | ||
| baseVersionCode = 1 | ||
| libVersion = "1.4" | ||
| baseVersionCode = 2 | ||
| libVersion = "1.6" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Page> { | ||
| abstract suspend fun getViewerResponse(chapter: SChapter): Response | ||
|
|
||
| override suspend fun getPageList(chapter: SChapter): List<Page> { | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. headers are passed implicitly, no need to pass them yourself |
||
| val viewerToken = tokenResponse.parseAs<TokenResponse>().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<MetaResponse>().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<PreprocessSettings>().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") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| private val preferences by getPreferencesLazy() | ||||||
| private val rscHeaders = headersBuilder() | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| .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()) | ||||||
|
Comment on lines
+38
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. call super.configureClient instead |
||||||
| 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<RankingResponse>() | ||||||
| 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<SChapter>, | ||||||
| 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) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you have all the info already, return both unconditionally |
||||||
| 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<ChapterListResponse>() | ||||||
| result?.toSChapterList(hideLocked).orEmpty().reversed() | ||||||
| } else { | ||||||
| chapters | ||||||
| } | ||||||
|
|
||||||
| override fun chapterListParse(response: Response): List<SChapter> { | ||||||
| val hideLocked = preferences.getBoolean(HIDE_LOCKED_PREF_KEY, false) | ||||||
| val result = response.extractNextJs<ChapterListResponse>() | ||||||
| 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 { | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
referer is added by default