Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5f94113
Fix AComics, MangaLib, and InkStory reading
biglag Sep 25, 2026
040970f
cleanup
biglag Sep 25, 2026
7bfb0c0
remove local worktree
biglag Sep 25, 2026
dd0da33
fix stuck homepage for Manga18fx
Fixer-kun Sep 26, 2026
b027e4e
Update Manga18fx.kt
Fixer-kun Sep 26, 2026
1d13779
Update Manga18fx.kt
Fixer-kun Sep 26, 2026
f4676e7
Merge pull request #94 from Fixer-kun/master
InvalidDavid Sep 26, 2026
4fe6a18
fix mangaball
InvalidDavid Sep 27, 2026
123e9c1
fix description
InvalidDavid Sep 27, 2026
1994bfc
fix images
InvalidDavid Sep 27, 2026
ca544cc
fix poor quality images
InvalidDavid Sep 27, 2026
d46f037
fix
InvalidDavid Sep 27, 2026
3c6a6a3
Update Manhuaus.kt
InvalidDavid Sep 27, 2026
cafd79d
Update TopManhua.kt
InvalidDavid Sep 27, 2026
fb60db4
Update ManhwaTop.kt
InvalidDavid Sep 27, 2026
936eb7a
Update KeyoApp.kt
InvalidDavid Sep 27, 2026
544dfef
Update TempleScan.kt
InvalidDavid Sep 27, 2026
36a5f7e
Update MangaKParser.kt
InvalidDavid Sep 27, 2026
8c3b023
Update NHentaiParser.kt
InvalidDavid Sep 27, 2026
750347d
Update MangaThemesia.kt
InvalidDavid Sep 27, 2026
a5803a3
Create ManhwaDesu.kt
InvalidDavid Sep 27, 2026
72b00e8
Create MmrcmsParser.kt
InvalidDavid Sep 27, 2026
8c3eb40
Create ReadComicsOnline.kt
InvalidDavid Sep 27, 2026
b1d9078
Fix MangaLib timeout and MangaBall details rating
biglag Sep 28, 2026
192aaa0
Merge remote-tracking branch 'upstream/testing' into fix/fix-russia-s…
biglag Sep 28, 2026
9775ce3
Create NexusToons.kt
InvalidDavid Sep 28, 2026
eda3fb7
Update NexusToons.kt
InvalidDavid Sep 28, 2026
65a6601
Merge pull request #93 from biglag/fix/fix-russia-sources
InvalidDavid Sep 28, 2026
baafed8
Update .gitignore
InvalidDavid Sep 28, 2026
025653c
fix templescan again
InvalidDavid Sep 28, 2026
5ecf130
Update VoraToon.kt
InvalidDavid Sep 28, 2026
f2d80a8
Update ReadComicsOnline.kt
InvalidDavid Sep 28, 2026
4d5be72
Create HanamiHeaven.kt
InvalidDavid Sep 28, 2026
c55aaca
mikoroku by kurkitsune768
InvalidDavid Sep 28, 2026
58984ac
Create MangaDNA.kt
InvalidDavid Sep 28, 2026
6140458
Create XCOMIC.kt
InvalidDavid Sep 28, 2026
c4b9bcf
Merge pull request #95 from InvalidDavid/testing
InvalidDavid Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ src/test/resources/cookies.txt
.vscode/

# Config file
/local.properties
/local.properties
15 changes: 6 additions & 9 deletions src/main/kotlin/tsuki/parsers/KeyoApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.EnumSet
import java.util.Locale
import java.util.TimeZone

abstract class KeyoApp(
context: MangaLoaderContext,
Expand All @@ -44,7 +43,7 @@ abstract class KeyoApp(
override val webClient = OkHttpWebClient(context.httpClient, source)

protected val baseUrl get() = "https://$domain"
protected open val dateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH)
protected open val dateFormat: String = "MMM d, yyyy"

protected open val showPaidChapters: Boolean = false

Expand Down Expand Up @@ -258,7 +257,6 @@ abstract class KeyoApp(

protected open val descriptionSelector: String = "#expand_content p"
protected open val altNameSelector: String = "div.font-medium:containsOwn(Alternative titles) ~ div span"
protected open val altNamePrefix: String = "Alternative names:"
protected open val statusSelector: String = "div:has(span:containsOwn(Status)) ~ div"
protected open val authorSelector: String = "div:has(span:containsOwn(Author)) ~ div"
protected open val artistSelector: String = "div:has(span:containsOwn(Artist)) ~ div"
Expand Down Expand Up @@ -415,10 +413,9 @@ abstract class KeyoApp(
if (text.isEmpty()) return 0L
if (text.contains("ago", ignoreCase = true)) return parseRelativeDate()
return try {
LocalDate.parse(text, dateFormat)
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli()
SimpleDateFormat(dateFormat, Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.parse(text)?.time ?: 0L
} catch (_: Exception) {
0L
}
Expand Down
19 changes: 12 additions & 7 deletions src/main/kotlin/tsuki/parsers/LibSocialParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import androidx.collection.ScatterMap
import androidx.collection.intObjectMapOf
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
Expand All @@ -69,19 +68,21 @@ internal abstract class LibSocialParser(
siteDomains: String,
) : PagedMangaParser(context, source, pageSize = 60), MangaParserAuthProvider {

protected val apiHost = "api.cdnlibs.org"
protected open val apiHost = "api.cdnlibs.org"

@Volatile
private var accessToken: String? = null

override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_MOBILE)

override val authUrl: String
get() = "https://$domain/ru/front/auth"

override suspend fun isAuthorized(): Boolean {
val token = getAuthData()?.optJSONObject("token")?.getStringOrNull("access_token")
return !token.isNullOrEmpty()
return !refreshAuthData()?.optJSONObject("token")?.getStringOrNull("access_token").isNullOrEmpty()
}

override suspend fun getUsername(): String = getAuthData()
override suspend fun getUsername(): String = refreshAuthData()
?.getJSONObject("auth")
?.getString("username")
?: throw AuthRequiredException(source)
Expand Down Expand Up @@ -117,7 +118,7 @@ internal abstract class LibSocialParser(
)

override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking { getAuthData() }?.optJSONObject("token")?.getStringOrNull("access_token")
val token = accessToken
val requestBuilder = chain.request().newBuilder()
if (!token.isNullOrEmpty()) {
requestBuilder.header("Authorization", "Bearer $token")
Expand Down Expand Up @@ -257,7 +258,7 @@ internal abstract class LibSocialParser(
val json = pages.await()
val primaryServer = getPrimaryImageServer(servers)
json.getJSONArray("pages").mapJSON { jo ->
val url = jo.getString("url")
val url = "/" + jo.getString("url").trimStart('/')
MangaPage(
id = generateUid(jo.getLong("id")),
url = concatUrl(primaryServer, url),
Expand Down Expand Up @@ -460,6 +461,10 @@ internal abstract class LibSocialParser(
return result
}

private suspend fun refreshAuthData(): JSONObject? = getAuthData().also { authData ->
accessToken = authData?.optJSONObject("token")?.getStringOrNull("access_token")
}

private suspend fun getAuthData(): JSONObject? {
val raw = WebViewHelper(context).getLocalStorageValue(domain, "auth") ?: return null
return JSONObject(raw.unescapeJson().removeSurrounding('"'))
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/tsuki/parsers/MangaKParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ internal abstract class MangaKParser(
}
}

filter.author?.trim()?.takeIf { it.isNotEmpty() }?.let { author ->
append("&author=")
append(author.urlEncoded())
}

if (filter.tags.isNotEmpty()) {
append("&genres=")
append(filter.tags.joinToString(",") { it.key })
Expand Down
81 changes: 77 additions & 4 deletions src/main/kotlin/tsuki/parsers/MangaThemesia.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Headers
import org.json.JSONArray
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
Expand All @@ -55,9 +56,16 @@ abstract class MangaThemesia(
keys.add(userAgentKey)
}

override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.set(CommonHeaders.REFERER, "https://$domain/")
.build()
override fun getRequestHeaders(): Headers {
val builder = super.getRequestHeaders().newBuilder()
.set(CommonHeaders.REFERER, "https://$domain/")

val userAgent = builder["User-Agent"]
?: config[userAgentKey]
applyClientHints(builder, userAgent)

return builder.build()
}

protected open val mangaDirectory = "manga"
protected open val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("en"))
Expand Down Expand Up @@ -429,7 +437,9 @@ abstract class MangaThemesia(
)
}

protected fun Element.imgAttr(): String {
protected open fun Element.imgAttr(): String = imgAttrFallback()

protected fun Element.imgAttrFallback(): String {
for (attr in listOf("data-src", "data-lazy-src", "data-original", "data-cfsrc", "data-image", "src")) {
val value = attr(attr).trim().takeIf { it.isNotEmpty() } ?: continue
return value.toAbsoluteUrl(domain).substringBefore("?") // remove WP resize params
Expand All @@ -447,7 +457,70 @@ abstract class MangaThemesia(
private fun SimpleDateFormat.parseSafe(date: String): Long =
runCatching { parse(date)?.time ?: 0L }.getOrDefault(0L)

private fun applyClientHints(builder: Headers.Builder, userAgent: String) {
val info = detectBrowser(userAgent) ?: return

val isMobile = userAgent.contains("Mobile") ||
userAgent.contains("Android") ||
userAgent.contains("iPhone") ||
userAgent.contains("iPad")

builder["Sec-CH-UA"] = buildSecChUa(info)
builder["Sec-CH-UA-Mobile"] = if (isMobile) "?1" else "?0"
builder["Sec-CH-UA-Platform"] = detectPlatform(userAgent)
builder["DNT"] = "1"
}

/** Firefox and Safari don't send client hints*/
private fun detectBrowser(userAgent: String): BrowserInfo? = when {
userAgent.contains("Firefox/") && !userAgent.contains("Chrome") -> null
userAgent.contains("Safari/") &&
!userAgent.contains("Chrome") &&
!userAgent.contains("Chromium") -> null

userAgent.contains("Edg/") || userAgent.contains("EdgA/") || userAgent.contains("EdgiOS/") -> {
val edgeVersion = EDGE_REGEX.find(userAgent)?.groupValues?.get(1) ?: "134"
val chromiumVersion = CHROME_REGEX.find(userAgent)?.groupValues?.get(1) ?: edgeVersion
BrowserInfo("Microsoft Edge", edgeVersion, chromiumVersion)
}

userAgent.contains("OPR/") -> {
val operaVersion = OPERA_REGEX.find(userAgent)?.groupValues?.get(1) ?: "118"
val chromiumVersion = CHROME_REGEX.find(userAgent)?.groupValues?.get(1) ?: "134"
BrowserInfo("Opera", operaVersion, chromiumVersion)
}

userAgent.contains("Chrome/") -> {
val chromeVersion = CHROME_REGEX.find(userAgent)?.groupValues?.get(1) ?: "134"
BrowserInfo("Google Chrome", chromeVersion, chromeVersion)
}

else -> null
}

private fun detectPlatform(userAgent: String): String = when {
userAgent.contains("Windows") -> "\"Windows\""
userAgent.contains("Android") -> "\"Android\""
userAgent.contains("iPhone") || userAgent.contains("iPad") -> "\"iOS\""
userAgent.contains("Macintosh") || userAgent.contains("Mac OS X") -> "\"macOS\""
userAgent.contains("Linux") -> "\"Linux\""
else -> "\"Windows\""
}

private fun buildSecChUa(info: BrowserInfo): String =
"\"${info.name}\";v=\"${info.version}\", \"Chromium\";v=\"${info.chromiumVersion}\", \"Not A(Brand\";v=\"24\""

private data class BrowserInfo(
val name: String,
val version: String,
val chromiumVersion: String,
)

companion object {
private val CHROME_REGEX = Regex("""Chrome/(\d+)""")
private val EDGE_REGEX = Regex("""Edg[^/]*/(\d+)""")
private val OPERA_REGEX = Regex("""OPR/(\d+)""")

private val AUTHOR_PLACEHOLDERS = setOf("n/a", "N/A", "Updating")

private val ongoingWordSet = WordSet(
Expand Down
Loading
Loading