From 02a2305c7339852aa998d6a1fa89b4209cf7ca88 Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:08:32 +0700 Subject: [PATCH 1/6] Ariverse: Add deeplink, move to lib 1.6, some improve code --- src/vi/ariverse/build.gradle.kts | 8 +- .../extension/vi/ariverse/Ariverse.kt | 374 ++++++++---------- .../tachiyomi/extension/vi/ariverse/Dto.kt | 4 +- .../extension/vi/ariverse/Filters.kt | 55 +-- 4 files changed, 202 insertions(+), 239 deletions(-) diff --git a/src/vi/ariverse/build.gradle.kts b/src/vi/ariverse/build.gradle.kts index 2c20f201ac4..01028822156 100644 --- a/src/vi/ariverse/build.gradle.kts +++ b/src/vi/ariverse/build.gradle.kts @@ -6,9 +6,9 @@ plugins { keiyoushi { name = "Ariverse" - versionCode = 53 + versionCode = 54 contentWarning = ContentWarning.MIXED - libVersion = "1.4" + libVersion = "1.6" source { lang = "vi" @@ -17,4 +17,8 @@ keiyoushi { } id = 4480433466073326866 } + + deeplink { + path("/comic/story/..*") + } } diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt index 55d6f94f1aa..508044b3232 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt @@ -1,45 +1,39 @@ package eu.kanade.tachiyomi.extension.vi.ariverse -import android.annotation.SuppressLint -import android.app.Application import android.content.SharedPreferences -import android.os.Handler -import android.os.Looper -import android.webkit.JavascriptInterface -import android.webkit.WebSettings -import android.webkit.WebView -import android.webkit.WebViewClient import androidx.preference.CheckBoxPreference import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.network.GET 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.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.model.SMangaUpdate import keiyoushi.annotation.Source +import keiyoushi.network.get import keiyoushi.network.rateLimit +import keiyoushi.source.KeiSource +import keiyoushi.utils.getLocalStorage import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs import keiyoushi.utils.tryParse +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import okhttp3.Headers +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response +import okhttp3.OkHttpClient import org.jsoup.Jsoup -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit @Source abstract class Ariverse : - HttpSource(), + KeiSource(), ConfigurableSource { private val apiUrl get() = baseUrl.replace("https://", "https://be.") + "/api/v1" @@ -52,97 +46,34 @@ abstract class Ariverse : private var cachedAuthToken: String? = null - private val webViewUserAgent: String? by lazy { - runCatching { WebSettings.getDefaultUserAgent(Injekt.get()) } - .getOrNull() - ?.let { it.replace(WEBVIEW_TOKEN_REGEX, ")") } - ?.takeIf { it.isNotBlank() } - } - - override val client = network.client.newBuilder() - .addInterceptor(authInterceptor()) - .rateLimit(3) - .build() + private val allowR18 get() = preferences.getBoolean("pref_r18", false) - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - .add("Accept", "application/json") - .apply { webViewUserAgent?.let { set("User-Agent", it) } } + // ============================== Client ================================ - private fun apiHeaders() = headersBuilder().apply { - authToken?.let { set("Authorization", "Bearer $it") } - }.build() + override fun OkHttpClient.Builder.configureClient(): OkHttpClient.Builder = apply { + addInterceptor(authInterceptor()) + rateLimit(3) + } - private val allowR18 get() = preferences.getBoolean("pref_r18", false) + override fun Headers.Builder.configureHeaders(): Headers.Builder = apply { + set("Referer", "$baseUrl/") + set("Accept", "application/json") + } // ============================== Auth ==================================== private val authToken: String? @Synchronized get() = cachedAuthToken - ?: getTokenFromWebView()?.also { cachedAuthToken = it } - - @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") - private fun getTokenFromWebView(): String? { - val handler = Handler(Looper.getMainLooper()) - val latch = CountDownLatch(1) - val interfaceName = randomJavascriptInterfaceName() - val bridge = TokenBridge(latch) - var webView: WebView? = null - - handler.post { - webView = WebView(Injekt.get()).apply { - with(settings) { - javaScriptEnabled = true - domStorageEnabled = true - databaseEnabled = true - blockNetworkImage = true - webViewUserAgent?.let { userAgentString = it } - } - addJavascriptInterface(bridge, interfaceName) - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - view?.evaluateJavascript( - """ - (function() { - try { - var token = localStorage.getItem('token'); - window['$interfaceName'].onToken(token || ''); - } catch(e) { - window['$interfaceName'].onToken(''); - } - })(); - """.trimIndent(), - null, - ) - } - } - loadDataWithBaseURL(baseUrl, " ", "text/html", "UTF-8", null) + ?: runBlocking { + runCatching { + getLocalStorage(baseUrl, "token") + ?.takeIf { it.isNotBlank() } + ?.also { cachedAuthToken = it } + }.getOrNull() } - } - latch.await(10, TimeUnit.SECONDS) - - handler.post { - webView?.removeJavascriptInterface(interfaceName) - webView?.destroy() - } - - return bridge.token?.takeIf { it.isNotBlank() } - } - - private class TokenBridge(private val latch: CountDownLatch) { - @Volatile - var token: String? = null - - @JavascriptInterface - fun onToken(value: String?) { - token = value - latch.countDown() - } - } - - private fun authInterceptor() = Interceptor { chain -> + private fun authInterceptor() = okhttp3.Interceptor { chain -> val original = chain.request() val token = authToken @@ -157,42 +88,38 @@ abstract class Ariverse : chain.proceed(request) } - private fun randomJavascriptInterfaceName(): String { - val pool = ('a'..'z') + ('A'..'Z') - return (1..(10..20).random()) - .map { pool.random() } - .joinToString("") - } - // ============================== Popular =============================== - override fun popularMangaRequest(page: Int): Request { + override suspend fun getPopularManga(page: Int): MangasPage { val url = "$apiUrl/stories/hot".toHttpUrl().newBuilder() .addQueryParameter("type", "comic") .addQueryParameter("is_18_plus", if (allowR18) "1" else "0") .addQueryParameter("limit", "50") .addQueryParameter("period", "week") .build() - return GET(url, apiHeaders()) + val result = client.get(url).parseAs() + + val mangas = result.data.map { createManga(it) } + + return MangasPage(mangas, false) } - override fun popularMangaParse(response: Response): MangasPage { - val result = response.parseAs() + private fun createManga(story: Story): SManga = SManga.create().apply { + setUrlWithoutDomain("/comic/story/${story.slug}") + title = story.title + thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + } - val mangas = result.data.map { story -> - SManga.create().apply { - url = "/comic/story/${story.slug}" - title = story.title - thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } - } + private fun resolveCoverUrl(coverPath: String): String { + if (coverPath.startsWith("http://") || coverPath.startsWith("https://")) { + return coverPath } - - return MangasPage(mangas, false) + return "$imageUrl/${coverPath.replace("\\", "/")}" } // ============================== Latest ================================ - override fun latestUpdatesRequest(page: Int): Request { + override suspend fun getLatestUpdates(page: Int): MangasPage { val url = "$apiUrl/stories".toHttpUrl().newBuilder() .addQueryParameter("type", "comic") .addQueryParameter("is_18_plus", if (allowR18) "1" else "0") @@ -201,14 +128,17 @@ abstract class Ariverse : .addQueryParameter("per_page", "50") .addQueryParameter("page", page.toString()) .build() - return GET(url, apiHeaders()) + val result = client.get(url).parseAs() + return parseMangaPage(result) } - override fun latestUpdatesParse(response: Response): MangasPage = parseMangaPage(response) - // ============================== Search ================================ - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + override suspend fun getSearchMangaList( + page: Int, + query: String, + filters: FilterList, + ): MangasPage { val url = "$apiUrl/stories".toHttpUrl().newBuilder().apply { addQueryParameter("type", "comic") addQueryParameter("is_18_plus", if (allowR18) "1" else "0") @@ -241,69 +171,63 @@ abstract class Ariverse : } }.build() - return GET(url, apiHeaders()) + val result = client.get(url).parseAs() + return parseMangaPage(result) } - override fun searchMangaParse(response: Response): MangasPage = parseMangaPage(response) + private fun parseMangaPage(result: StoryListResponse): MangasPage { + val mangas = result.data.map { createManga(it) } - private fun parseMangaPage(response: Response): MangasPage { - val result = response.parseAs() + val hasNextPage = result.currentPage < result.lastPage + + return MangasPage(mangas, hasNextPage) + } - val mangas = result.data.map { story -> - SManga.create().apply { - url = "/comic/story/${story.slug}" + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + val slug = url.pathSegments.lastOrNull() ?: return null + + val story = runCatching { + client.get("$apiUrl/stories/$slug") + .parseAs().data + }.getOrNull() + + return SManga.create().apply { + setUrlWithoutDomain("/comic/story/$slug") + if (story != null) { title = story.title thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + author = story.author + artist = story.artist + description = story.description?.let { parseDescription(it) } + genre = story.genres?.joinToString { it.name } + status = parseStatus(story.status) } + initialized = true } - - val hasNextPage = result.currentPage < result.lastPage - - return MangasPage(mangas, hasNextPage) } // ============================== Filters =============================== - override fun getFilterList(): FilterList = getFilters() + override val supportsFilterFetching get() = true - // ============================== Details =============================== + override suspend fun fetchFilterData(): JsonElement = client.get("$apiUrl/genres").parseAs() - override fun mangaDetailsRequest(manga: SManga): Request { - val slug = manga.url.substringAfterLast("/") - val url = "$apiUrl/stories/$slug".toHttpUrl() - return GET(url, apiHeaders()) + override fun getFilterList(data: JsonElement?): FilterList { + val genres = data?.jsonObject?.get("data") as? JsonArray + ?: return buildGenreFilter(null) + return buildGenreFilter(genres) } - override fun mangaDetailsParse(response: Response): SManga { - val story = response.parseAs().data - - return SManga.create().apply { - url = "/comic/story/${story.slug}" - title = story.title - thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } - author = story.author - artist = story.artist - description = story.description?.let { parseDescription(it) } - genre = story.genres?.joinToString { it.name } - status = parseStatus(story.status) - } - } - - private fun resolveCoverUrl(coverPath: String): String { - if (coverPath.startsWith("http://") || coverPath.startsWith("https://")) { - return coverPath - } - return "$imageUrl/${coverPath.replace("\\", "/")}" - } + // ============================== Details + Chapters ==================== private fun parseDescription(html: String): String { val normalized = html - .replace(BR_TAG_REGEX, "\n") + .replace(brTagRegex, "\n") .replace(" ", " ") return Jsoup.parse(normalized).wholeText() - .replace(HORIZONTAL_SPACE_REGEX, " ") - .replace(MULTI_NEWLINE_REGEX, "\n") + .replace(horizontalSpaceRegex, " ") + .replace(multiNewlineRegex, "\n") } private fun parseStatus(status: String?): Int = when { @@ -313,44 +237,77 @@ abstract class Ariverse : else -> SManga.UNKNOWN } - // ============================== Chapters ============================== + override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url - override fun chapterListRequest(manga: SManga): Request { + override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url + + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { val slug = manga.url.substringAfterLast("/") - val url = "$apiUrl/stories/$slug/chapters".toHttpUrl() - return GET(url, apiHeaders()) - } - override fun chapterListParse(response: Response): List { - val chapterData = response.parseAs().data - - return chapterData.chapters - .sortedByDescending { it.number } - .map { chapter -> - SChapter.create().apply { - url = "/comic/story/${chapterData.story.slug}/${chapter.slug}" - name = chapter.title - chapter_number = chapter.number.toFloat() - date_upload = chapter.publishedAt?.let { DATE_FORMAT.tryParse(it) } ?: 0L + val updatedManga = if (fetchDetails) { + val story = runCatching { + client.get("$apiUrl/stories/$slug") + .parseAs().data + }.getOrNull() + + if (story != null) { + SManga.create().apply { + setUrlWithoutDomain("/comic/story/${story.slug}") + title = story.title + thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + author = story.author + artist = story.artist + description = story.description?.let { parseDescription(it) } + genre = story.genres?.joinToString { it.name } + status = parseStatus(story.status) } + } else { + manga } + } else { + manga + } + + val updatedChapters = if (fetchChapters) { + val chapterData = client.get("$apiUrl/stories/$slug/chapters") + .parseAs().data + + chapterData.chapters + .sortedByDescending { it.number } + .map { chapter -> + SChapter.create().apply { + setUrlWithoutDomain("/comic/story/${chapterData.story.slug}/${chapter.slug}") + name = chapter.title ?: "${chapter.number.toInt()}" + chapter_number = chapter.number.toFloat() + date_upload = chapter.publishedAt?.let { dateFormat.tryParse(it) } ?: 0L + } + } + } else { + chapters + } + + return SMangaUpdate( + manga = updatedManga, + chapters = updatedChapters, + ) } // ============================== Pages ================================= - override fun pageListRequest(chapter: SChapter): Request { + override suspend fun getPageList(chapter: SChapter): List { val parts = chapter.url.trim('/').split("/") val storySlug = parts.getOrElse(2) { "" } val chapterSlug = parts.getOrElse(3) { "" } val url = "$apiUrl/stories/$storySlug/chapters/$chapterSlug".toHttpUrl() - return GET(url, apiHeaders()) - } - - override fun pageListParse(response: Response): List { - val chapterDetail = response.parseAs().data + val chapterDetail = client.get(url).parseAs().data if (chapterDetail.contentLocked) { - throw Exception(LOGIN_WEBVIEW_MESSAGE) + throw Exception(loginWebviewMessage) } val content = chapterDetail.content.orEmpty() @@ -364,7 +321,30 @@ abstract class Ariverse : } } - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + // ============================== Related =============================== + + override suspend fun fetchRelatedMangaList(manga: SManga): List { + val slug = manga.url.substringAfterLast("/") + val story = runCatching { + client.get("$apiUrl/stories/$slug") + .parseAs().data + }.getOrNull() ?: return emptyList() + + val firstGenreSlug = story.genres?.firstOrNull()?.slug ?: return emptyList() + + val result = client.get( + "$apiUrl/stories".toHttpUrl().newBuilder() + .addQueryParameter("type", "comic") + .addQueryParameter("genre", firstGenreSlug) + .addQueryParameter("per_page", "12") + .addQueryParameter("page", "1") + .build(), + ).parseAs() + + return result.data + .filter { it.slug != slug } + .map { createManga(it) } + } // ============================== Settings ============================== @@ -377,19 +357,15 @@ abstract class Ariverse : }.let(screen::addPreference) } - companion object { - private val BR_TAG_REGEX = Regex("""""", RegexOption.IGNORE_CASE) - private val HORIZONTAL_SPACE_REGEX = Regex("[\\t\\x0B\\f\\r ]+") - private val MULTI_NEWLINE_REGEX = Regex("\\n{2,}") + private val brTagRegex = Regex("""""", RegexOption.IGNORE_CASE) + private val horizontalSpaceRegex = Regex("[\\t\\x0B\\f\\r ]+") + private val multiNewlineRegex = Regex("\\n{2,}") - private val DATE_FORMAT by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply { - timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") - } + private val dateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply { + timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") } - - private val WEBVIEW_TOKEN_REGEX = Regex(""";\s*wv\)""") - - private const val LOGIN_WEBVIEW_MESSAGE = "Vui lòng đăng nhập vào tài khoản phù hợp qua Webview để đọc chương này" } + + private val loginWebviewMessage = "Vui lòng đăng nhập vào tài khoản phù hợp qua Webview để đọc chương này" } diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Dto.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Dto.kt index d6cd0132c9d..84010a79aab 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Dto.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Dto.kt @@ -98,7 +98,7 @@ class Chapter( @SerialName("story_id") val storyId: Int, val volume: Int? = null, val number: Double, - val title: String, + val title: String? = null, val slug: String, @SerialName("is_public") val isPublic: Boolean = true, @SerialName("published_at") val publishedAt: String? = null, @@ -115,7 +115,7 @@ class ChapterDetail( @SerialName("story_id") val storyId: Int, val volume: Int? = null, val number: Double, - val title: String, + val title: String? = null, val slug: String, @SerialName("is_public") val isPublic: Boolean = true, @SerialName("published_at") val publishedAt: String? = null, diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt index 0c80223a419..9b3951a9bdd 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt @@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.extension.vi.ariverse import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive class GenreFilter( name: String, @@ -58,40 +62,19 @@ class SortFilter : } } -private val GENRE_LIST = listOf( - "18+" to "18", - "Âu Cổ" to "au-co", - "Cách biệt tuổi tác" to "cach-biet-tuoi-tac", - "Chiếm hữu" to "chiem-huu", - "Cổ trang" to "co-trang", - "Drama" to "drama", - "Fantasy" to "fantasy", - "Girl Crush" to "girl-crush", - "Hài hước" to "hai-huoc", - "Học đường" to "hoc-duong", - "Horror" to "horror", - "Idol" to "idol", - "Mafia" to "mafia", - "Manhua" to "manhua", - "Manhwa" to "manhwa", - "Mọt sách" to "mot-sach", - "Mùa hè và biển cả" to "mua-he-va-bien-ca", - "Người lớn" to "nguoi-lon", - "Nhẹ nhàng" to "nhe-nhang", - "One-shot" to "one-shot", - "Quan hệ Chủ-Tớ" to "quan-he-chu-to", - "Thanh Xuân Mùa Hạ" to "thanh-xuan-mua-ha", - "Tình tay ba" to "tinh-tay-ba", - "Tội phạm" to "toi-pham", - "Trả thù" to "tra-thu", - "Truyện Tranh" to "truyen-tranh", - "Tuyết Đầu Mùa" to "tuyet-dau-mua", - "Văn phòng" to "van-phong", - "Xã hội đen" to "xa-hoi-den", -) +fun buildGenreFilter(data: JsonElement?): FilterList { + val genres = (data as? JsonArray) + ?.mapNotNull { + val obj = it.jsonObject + val name = obj["name"]?.jsonPrimitive?.content ?: return@mapNotNull null + val slug = obj["slug"]?.jsonPrimitive?.content ?: return@mapNotNull null + name to slug + } + .orEmpty() -fun getFilters(): FilterList = FilterList( - GenreFilter("Genre", GENRE_LIST), - StatusFilter(), - SortFilter(), -) + return FilterList( + GenreFilter("Genre", genres), + StatusFilter(), + SortFilter(), + ) +} From 7ae95760a6f84808e9a509a36669d1a774d6ba16 Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:39:53 +0700 Subject: [PATCH 2/6] update suggestion Co-Authored-By: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../extension/vi/ariverse/Ariverse.kt | 133 +++++++----------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt index 508044b3232..4bb71fef0a3 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt @@ -14,22 +14,17 @@ import keiyoushi.annotation.Source import keiyoushi.network.get import keiyoushi.network.rateLimit import keiyoushi.source.KeiSource +import keiyoushi.utils.array +import keiyoushi.utils.get import keiyoushi.utils.getLocalStorage import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs -import keiyoushi.utils.tryParse -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonObject -import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import org.jsoup.Jsoup -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone +import kotlin.time.Instant @Source abstract class Ariverse : @@ -40,12 +35,8 @@ abstract class Ariverse : private val imageUrl get() = baseUrl.replace("https://", "https://img.") - override val supportsLatest = true - private val preferences: SharedPreferences = getPreferences() - private var cachedAuthToken: String? = null - private val allowR18 get() = preferences.getBoolean("pref_r18", false) // ============================== Client ================================ @@ -55,42 +46,36 @@ abstract class Ariverse : rateLimit(3) } - override fun Headers.Builder.configureHeaders(): Headers.Builder = apply { - set("Referer", "$baseUrl/") - set("Accept", "application/json") - } - // ============================== Auth ==================================== - private val authToken: String? - @Synchronized - get() = cachedAuthToken - ?: runBlocking { - runCatching { - getLocalStorage(baseUrl, "token") - ?.takeIf { it.isNotBlank() } - ?.also { cachedAuthToken = it } - }.getOrNull() - } + private var cachedAuthToken: String? = null - private fun authInterceptor() = okhttp3.Interceptor { chain -> - val original = chain.request() - val token = authToken + private val storyCache = mutableMapOf() - val request = if (token != null) { - original.newBuilder() - .header("Authorization", "Bearer $token") - .build() - } else { - original - } + private suspend fun loadAuthToken() { + if (cachedAuthToken != null) return + val token = runCatching { + getLocalStorage(baseUrl, "token") + ?.takeIf { it.isNotBlank() } + }.getOrNull() + cachedAuthToken = token + } + private fun authInterceptor() = okhttp3.Interceptor { chain -> + val original = chain.request() + val request = original.newBuilder().apply { + cachedAuthToken?.let { header("Authorization", "Bearer $it") } + if (original.url.encodedPath.startsWith("/api/")) { + header("Accept", "application/json") + } + }.build() chain.proceed(request) } // ============================== Popular =============================== override suspend fun getPopularManga(page: Int): MangasPage { + loadAuthToken() val url = "$apiUrl/stories/hot".toHttpUrl().newBuilder() .addQueryParameter("type", "comic") .addQueryParameter("is_18_plus", if (allowR18) "1" else "0") @@ -186,22 +171,18 @@ abstract class Ariverse : override suspend fun getMangaByUrl(url: HttpUrl): SManga? { val slug = url.pathSegments.lastOrNull() ?: return null - val story = runCatching { - client.get("$apiUrl/stories/$slug") - .parseAs().data - }.getOrNull() + val story = client.get("$apiUrl/stories/$slug".toHttpUrl()) + .parseAs().data return SManga.create().apply { setUrlWithoutDomain("/comic/story/$slug") - if (story != null) { - title = story.title - thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } - author = story.author - artist = story.artist - description = story.description?.let { parseDescription(it) } - genre = story.genres?.joinToString { it.name } - status = parseStatus(story.status) - } + title = story.title + thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + author = story.author + artist = story.artist + description = story.description?.let { parseDescription(it) } + genre = story.genres?.joinToString { it.name } + status = parseStatus(story.status) initialized = true } } @@ -210,10 +191,10 @@ abstract class Ariverse : override val supportsFilterFetching get() = true - override suspend fun fetchFilterData(): JsonElement = client.get("$apiUrl/genres").parseAs() + override suspend fun fetchFilterData(): JsonElement = client.get("$apiUrl/genres".toHttpUrl()).parseAs() override fun getFilterList(data: JsonElement?): FilterList { - val genres = data?.jsonObject?.get("data") as? JsonArray + val genres = data["data"]?.array ?: return buildGenreFilter(null) return buildGenreFilter(genres) } @@ -250,31 +231,26 @@ abstract class Ariverse : val slug = manga.url.substringAfterLast("/") val updatedManga = if (fetchDetails) { - val story = runCatching { - client.get("$apiUrl/stories/$slug") - .parseAs().data - }.getOrNull() - - if (story != null) { - SManga.create().apply { - setUrlWithoutDomain("/comic/story/${story.slug}") - title = story.title - thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } - author = story.author - artist = story.artist - description = story.description?.let { parseDescription(it) } - genre = story.genres?.joinToString { it.name } - status = parseStatus(story.status) - } - } else { - manga + val story = client.get("$apiUrl/stories/$slug".toHttpUrl()) + .parseAs().data + storyCache[slug] = story + + SManga.create().apply { + setUrlWithoutDomain("/comic/story/${story.slug}") + title = story.title + thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + author = story.author + artist = story.artist + description = story.description?.let { parseDescription(it) } + genre = story.genres?.joinToString { it.name } + status = parseStatus(story.status) } } else { manga } val updatedChapters = if (fetchChapters) { - val chapterData = client.get("$apiUrl/stories/$slug/chapters") + val chapterData = client.get("$apiUrl/stories/$slug/chapters".toHttpUrl()) .parseAs().data chapterData.chapters @@ -284,7 +260,7 @@ abstract class Ariverse : setUrlWithoutDomain("/comic/story/${chapterData.story.slug}/${chapter.slug}") name = chapter.title ?: "${chapter.number.toInt()}" chapter_number = chapter.number.toFloat() - date_upload = chapter.publishedAt?.let { dateFormat.tryParse(it) } ?: 0L + date_upload = chapter.publishedAt?.let { Instant.parseOrNull(it)?.toEpochMilliseconds() } ?: 0L } } } else { @@ -325,10 +301,9 @@ abstract class Ariverse : override suspend fun fetchRelatedMangaList(manga: SManga): List { val slug = manga.url.substringAfterLast("/") - val story = runCatching { - client.get("$apiUrl/stories/$slug") - .parseAs().data - }.getOrNull() ?: return emptyList() + val story = storyCache[slug] + ?: client.get("$apiUrl/stories/$slug".toHttpUrl()) + .parseAs().data.also { storyCache[slug] = it } val firstGenreSlug = story.genres?.firstOrNull()?.slug ?: return emptyList() @@ -361,11 +336,5 @@ abstract class Ariverse : private val horizontalSpaceRegex = Regex("[\\t\\x0B\\f\\r ]+") private val multiNewlineRegex = Regex("\\n{2,}") - private val dateFormat by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply { - timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") - } - } - private val loginWebviewMessage = "Vui lòng đăng nhập vào tài khoản phù hợp qua Webview để đọc chương này" } From 09e7c3d3933f4531ca2c548057444adae8886734 Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Sat, 18 Jul 2026 01:20:05 +0700 Subject: [PATCH 3/6] update suggestion, add coroutineScope Co-Authored-By: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../extension/vi/ariverse/Ariverse.kt | 111 ++++++++++-------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt index 4bb71fef0a3..9eda4f11022 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt @@ -19,7 +19,12 @@ import keiyoushi.utils.get import keiyoushi.utils.getLocalStorage import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs +import keiyoushi.utils.string +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient @@ -49,16 +54,13 @@ abstract class Ariverse : // ============================== Auth ==================================== private var cachedAuthToken: String? = null - - private val storyCache = mutableMapOf() + private var authChecked = false private suspend fun loadAuthToken() { - if (cachedAuthToken != null) return - val token = runCatching { - getLocalStorage(baseUrl, "token") - ?.takeIf { it.isNotBlank() } - }.getOrNull() - cachedAuthToken = token + if (authChecked) return + authChecked = true + cachedAuthToken = getLocalStorage(baseUrl, "token") + ?.takeIf { it.isNotBlank() } } private fun authInterceptor() = okhttp3.Interceptor { chain -> @@ -171,7 +173,7 @@ abstract class Ariverse : override suspend fun getMangaByUrl(url: HttpUrl): SManga? { val slug = url.pathSegments.lastOrNull() ?: return null - val story = client.get("$apiUrl/stories/$slug".toHttpUrl()) + val story = client.get("$apiUrl/stories/$slug") .parseAs().data return SManga.create().apply { @@ -191,7 +193,7 @@ abstract class Ariverse : override val supportsFilterFetching get() = true - override suspend fun fetchFilterData(): JsonElement = client.get("$apiUrl/genres".toHttpUrl()).parseAs() + override suspend fun fetchFilterData(): JsonElement = client.get("$apiUrl/genres").parseAs() override fun getFilterList(data: JsonElement?): FilterList { val genres = data["data"]?.array @@ -230,41 +232,55 @@ abstract class Ariverse : ): SMangaUpdate { val slug = manga.url.substringAfterLast("/") - val updatedManga = if (fetchDetails) { - val story = client.get("$apiUrl/stories/$slug".toHttpUrl()) - .parseAs().data - storyCache[slug] = story - - SManga.create().apply { - setUrlWithoutDomain("/comic/story/${story.slug}") - title = story.title - thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } - author = story.author - artist = story.artist - description = story.description?.let { parseDescription(it) } - genre = story.genres?.joinToString { it.name } - status = parseStatus(story.status) + val updatedManga: SManga + val updatedChapters: List + + coroutineScope { + val mangaDeferred = if (fetchDetails) { + async { + val story = client.get("$apiUrl/stories/$slug") + .parseAs().data + + SManga.create().apply { + setUrlWithoutDomain("/comic/story/${story.slug}") + title = story.title + thumbnail_url = story.coverPath?.let { resolveCoverUrl(it) } + author = story.author + artist = story.artist + description = story.description?.let { parseDescription(it) } + genre = story.genres?.joinToString { it.name } + status = parseStatus(story.status) + memo = buildJsonObject { + story.genres?.firstOrNull()?.slug?.let { put("genreSlug", JsonPrimitive(it)) } + } + } + } + } else { + null } - } else { - manga - } - val updatedChapters = if (fetchChapters) { - val chapterData = client.get("$apiUrl/stories/$slug/chapters".toHttpUrl()) - .parseAs().data - - chapterData.chapters - .sortedByDescending { it.number } - .map { chapter -> - SChapter.create().apply { - setUrlWithoutDomain("/comic/story/${chapterData.story.slug}/${chapter.slug}") - name = chapter.title ?: "${chapter.number.toInt()}" - chapter_number = chapter.number.toFloat() - date_upload = chapter.publishedAt?.let { Instant.parseOrNull(it)?.toEpochMilliseconds() } ?: 0L - } + val chaptersDeferred = if (fetchChapters) { + async { + val chapterData = client.get("$apiUrl/stories/$slug/chapters") + .parseAs().data + + chapterData.chapters + .sortedByDescending { it.number } + .map { chapter -> + SChapter.create().apply { + setUrlWithoutDomain("/comic/story/${chapterData.story.slug}/${chapter.slug}") + name = chapter.title ?: "${chapter.number.toInt()}" + chapter_number = chapter.number.toFloat() + date_upload = chapter.publishedAt?.let { Instant.parseOrNull(it)?.toEpochMilliseconds() } ?: 0L + } + } } - } else { - chapters + } else { + null + } + + updatedManga = mangaDeferred?.await() ?: manga + updatedChapters = chaptersDeferred?.await() ?: chapters } return SMangaUpdate( @@ -301,16 +317,15 @@ abstract class Ariverse : override suspend fun fetchRelatedMangaList(manga: SManga): List { val slug = manga.url.substringAfterLast("/") - val story = storyCache[slug] - ?: client.get("$apiUrl/stories/$slug".toHttpUrl()) - .parseAs().data.also { storyCache[slug] = it } - - val firstGenreSlug = story.genres?.firstOrNull()?.slug ?: return emptyList() + val genreSlug = manga.memo["genreSlug"]?.string + ?: client.get("$apiUrl/stories/$slug") + .parseAs().data.genres?.firstOrNull()?.slug + ?: return emptyList() val result = client.get( "$apiUrl/stories".toHttpUrl().newBuilder() .addQueryParameter("type", "comic") - .addQueryParameter("genre", firstGenreSlug) + .addQueryParameter("genre", genreSlug) .addQueryParameter("per_page", "12") .addQueryParameter("page", "1") .build(), From 10af0a5b5a222471e8f9a589061f200fe03781ec Mon Sep 17 00:00:00 2001 From: fiorenmas <102145692+FiorenMas@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:49:50 +0700 Subject: [PATCH 4/6] Update suggestion Co-Authored-By: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- .../eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt | 5 +++++ .../src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt index 9eda4f11022..721af81f5a0 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Ariverse.kt @@ -107,6 +107,7 @@ abstract class Ariverse : // ============================== Latest ================================ override suspend fun getLatestUpdates(page: Int): MangasPage { + loadAuthToken() val url = "$apiUrl/stories".toHttpUrl().newBuilder() .addQueryParameter("type", "comic") .addQueryParameter("is_18_plus", if (allowR18) "1" else "0") @@ -126,6 +127,7 @@ abstract class Ariverse : query: String, filters: FilterList, ): MangasPage { + loadAuthToken() val url = "$apiUrl/stories".toHttpUrl().newBuilder().apply { addQueryParameter("type", "comic") addQueryParameter("is_18_plus", if (allowR18) "1" else "0") @@ -171,6 +173,7 @@ abstract class Ariverse : } override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + loadAuthToken() val slug = url.pathSegments.lastOrNull() ?: return null val story = client.get("$apiUrl/stories/$slug") @@ -230,6 +233,7 @@ abstract class Ariverse : fetchDetails: Boolean, fetchChapters: Boolean, ): SMangaUpdate { + loadAuthToken() val slug = manga.url.substringAfterLast("/") val updatedManga: SManga @@ -292,6 +296,7 @@ abstract class Ariverse : // ============================== Pages ================================= override suspend fun getPageList(chapter: SChapter): List { + loadAuthToken() val parts = chapter.url.trim('/').split("/") val storySlug = parts.getOrElse(2) { "" } val chapterSlug = parts.getOrElse(3) { "" } diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt index 9b3951a9bdd..1201c9c0742 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt @@ -66,8 +66,8 @@ fun buildGenreFilter(data: JsonElement?): FilterList { val genres = (data as? JsonArray) ?.mapNotNull { val obj = it.jsonObject - val name = obj["name"]?.jsonPrimitive?.content ?: return@mapNotNull null - val slug = obj["slug"]?.jsonPrimitive?.content ?: return@mapNotNull null + val name = obj["name"]?.string ?: return@mapNotNull null + val slug = obj["slug"]?.string ?: return@mapNotNull null name to slug } .orEmpty() From af4443bea390e134efd054dbe16a339a0157cae1 Mon Sep 17 00:00:00 2001 From: fiorenmas <102145692+FiorenMas@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:52:14 +0700 Subject: [PATCH 5/6] lint --- .../src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt index 1201c9c0742..b75c5a41341 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt @@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.model.FilterList import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive class GenreFilter( name: String, From a7ea776fb09fa525a0cb807f453a9f6b37cb3a13 Mon Sep 17 00:00:00 2001 From: fiorenmas <102145692+FiorenMas@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:56:18 +0700 Subject: [PATCH 6/6] more lint... --- .../src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt index b75c5a41341..3bef636a67c 100644 --- a/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt +++ b/src/vi/ariverse/src/eu/kanade/tachiyomi/extension/vi/ariverse/Filters.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.vi.ariverse import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList +import keiyoushi.utils.string import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject