Compare commits

..

No commits in common. "c66abf25b95e984663af0fcc0ed5cf6586fed3d4" and "07eba2f8f2890c1aa39196ce16d4fb771c5745c6" have entirely different histories.

702 changed files with 5213 additions and 25928 deletions

View File

@ -1,2 +0,0 @@
# Force \n line ending, trailing newline
b72776e1f528c56ae6a9ef3a6f8d72533b00d39f

View File

@ -99,7 +99,7 @@ body:
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose). - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
required: true required: true

View File

@ -1,18 +1,14 @@
# Keiyoushi Extensions
### Please give the repo a :star: ### Please give the repo a :star:
| Build | Need Help? | | Build | Support Server |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [![CI](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml/badge.svg)](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml) | [![Discord](https://img.shields.io/discord/1193460528052453448.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/3FbCpdKbdY) | | [![CI](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml/badge.svg)](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml) | [![Discord](https://img.shields.io/discord/1193460528052453448.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/3FbCpdKbdY) |
## Usage # Usage
**If you are new to repository/extensions, please read the [Keiyoushi Getting Started guide](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo) first.**
* You can add our repo by visiting the [Keiyoushi Website](https://keiyoushi.github.io/add-repo) [Getting started](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo)
* Otherwise, copy & paste the following URL: https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json
## Requests # Requests
To request a new source or bug fix, [create an issue](https://github.com/keiyoushi/extensions-source/issues/new/choose). To request a new source or bug fix, [create an issue](https://github.com/keiyoushi/extensions-source/issues/new/choose).
@ -23,7 +19,7 @@ difficult to maintain.
If you would like to see a request fulfilled and have the necessary skills to do so, consider contributing! If you would like to see a request fulfilled and have the necessary skills to do so, consider contributing!
Issues are up-for-grabs for any developer if there is no assigned user already. Issues are up-for-grabs for any developer if there is no assigned user already.
## Contributing # Contributing
Contributions are welcome! Contributions are welcome!

View File

@ -1,26 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android">
<application> <application>
<activity <activity
android:name=".all.globalcomix.GlobalComixUrlActivity" android:name="eu.kanade.tachiyomi.multisrc.a3manga.A3MangaUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
<intent-filter <intent-filter>
android:autoVerify="false"
tools:targetApi="23">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:host="globalcomix.com" /> <data android:host="${SOURCEHOST}" />
<data android:scheme="https" /> <data android:host="*.${SOURCEHOST}" />
<data android:pathPattern="/truyen-tranh/..*"
<data android:pathPattern="/c/..*" /> android:scheme="${SOURCESCHEME}" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View File

@ -0,0 +1,247 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
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.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
open class A3Manga(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest: Boolean = false
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/page/$page/", headers)
override fun popularMangaSelector() = ".comic-list .comic-item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select(".comic-title-link a").attr("href"))
title = element.select(".comic-title").text().trim()
thumbnail_url = element.select(".img-thumbnail").attr("abs:src")
}
override fun popularMangaNextPageSelector() = "li.next:not(.disabled)"
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
fetchMangaDetails(
SManga.create().apply {
url = "/truyen-tranh/$id/"
},
)
.map {
it.url = "/truyen-tranh/$id/"
MangasPage(listOf(it), false)
}
}
else -> super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
POST(
"$baseUrl/wp-admin/admin-ajax.php",
headers,
FormBody.Builder()
.add("action", "searchtax")
.add("keyword", query)
.build(),
)
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchResponseDto>()
if (!dto.success) {
return MangasPage(emptyList(), false)
}
val manga = dto.data
.filter { it.cstatus != "Nhóm dịch" }
.map {
SManga.create().apply {
setUrlWithoutDomain(it.link)
title = it.title
thumbnail_url = it.img
}
}
return MangasPage(manga, false)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".info-title").text()
author = document.select(".comic-info strong:contains(Tác giả) + span").text().trim()
description = document.select(".intro-container .text-justify").text().substringBefore("— Xem Thêm —")
genre = document.select(".comic-info .tags a").joinToString { tag ->
tag.text().split(' ').joinToString(separator = " ") { word ->
word.replaceFirstChar { it.titlecase() }
}
}
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
val statusString = document.select(".comic-info strong:contains(Tình trạng) + span").text()
status = when (statusString) {
"Đang tiến hành" -> SManga.ONGOING
"Trọn bộ " -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
override fun chapterListSelector(): String = ".chapter-table table tbody tr"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select("a .hidden-sm").text()
date_upload = runCatching {
dateFormat.parse(element.select("td").last()!!.text())?.time
}.getOrNull() ?: 0
}
protected fun decodeImgList(document: Document): String {
val htmlContentScript = document.selectFirst("script:containsData(htmlContent)")?.html()
?.substringAfter("var htmlContent=\"")
?.substringBefore("\";")
?.replace("\\\"", "\"")
?.replace("\\\\", "\\")
?.replace("\\/", "/")
?: throw Exception("Couldn't find script with image data.")
val htmlContent = json.decodeFromString<CipherDto>(htmlContentScript)
val ciphertext = Base64.decode(htmlContent.ciphertext, Base64.DEFAULT)
val iv = htmlContent.iv.decodeHex()
val salt = htmlContent.salt.decodeHex()
val passwordScript = document.selectFirst("script:containsData(chapterHTML)")?.html()
?: throw Exception("Couldn't find password to decrypt image data.")
val passphrase = passwordScript.substringAfter("var chapterHTML=CryptoJSAesDecrypt('")
.substringBefore("',htmlContent")
.replace("'+'", "")
val keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM)
val spec = PBEKeySpec(passphrase.toCharArray(), salt, 999, 256)
val keyS = SecretKeySpec(keyFactory.generateSecret(spec).encoded, "AES")
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv))
val imgListHtml = cipher.doFinal(ciphertext).toString(Charsets.UTF_8)
return imgListHtml
}
override fun pageListParse(document: Document): List<Page> {
val imgListHtml = decodeImgList(document)
return Jsoup.parseBodyFragment(imgListHtml).select("img").mapIndexed { idx, element ->
val encryptedUrl = element.attributes().find { it.key.startsWith("data") }?.value
val effectiveUrl = encryptedUrl?.decodeUrl() ?: element.attr("abs:src")
Page(idx, imageUrl = effectiveUrl)
}
}
private fun String.decodeUrl(): String? {
// We expect the URL to start with `https://`, where the last 3 characters are encoded.
// The length of the encoded character is not known, but it is the same across all.
// Essentially we are looking for the two encoded slashes, which tells us the length.
val patternIdx = patternsLengthCheck.indexOfFirst { pattern ->
val matchResult = pattern.find(this)
val g1 = matchResult?.groupValues?.get(1)
val g2 = matchResult?.groupValues?.get(2)
g1 == g2 && g1 != null
}
if (patternIdx == -1) {
return null
}
// With a known length we can predict all the encoded characters.
// This is a slightly more expensive pattern, hence the separation.
val matchResult = patternsSubstitution[patternIdx].find(this)
return matchResult?.destructured?.let { (colon, slash, period) ->
this
.replace(colon, ":")
.replace(slash, "/")
.replace(period, ".")
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
// https://stackoverflow.com/a/66614516
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
const val KEY_ALGORITHM = "PBKDF2WithHmacSHA512"
const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7PADDING"
const val PREFIX_ID_SEARCH = "id:"
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
}
private val patternsLengthCheck: List<Regex> = (20 downTo 1).map { i ->
"""^https.{$i}(.{$i})(.{$i})""".toRegex()
}
private val patternsSubstitution: List<Regex> = (20 downTo 1).map { i ->
"""^https(.{$i})(.{$i}).*(.{$i})(?:webp|jpeg|tiff|.{3})$""".toRegex()
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.vi.teamlanhlung package eu.kanade.tachiyomi.multisrc.a3manga
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.vi.teamlanhlung package eu.kanade.tachiyomi.multisrc.a3manga
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -10,7 +10,7 @@ import kotlin.system.exitProcess
/* /*
Springboard that accepts https://<domain>/truyen-tranh/$id/ intents Springboard that accepts https://<domain>/truyen-tranh/$id/ intents
*/ */
class TeamLanhLungUrlActivity : Activity() { class A3MangaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
@ -25,10 +25,10 @@ class TeamLanhLungUrlActivity : Activity() {
}, },
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e("TeamLanhLungUrlActivity", e.toString()) Log.e("A3MangaThemeUrlActivity", e.toString())
} }
} else { } else {
Log.e("TeamLanhLungUrlActivity", "Could not parse URI from intent $intent") Log.e("A3MangaThemeUrlActivity", "Could not parse URI from intent $intent")
} }
finish() finish()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,322 +0,0 @@
package eu.kanade.tachiyomi.multisrc.greenshit
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
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.util.asJsoup
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
abstract class GreenShit(
override val name: String,
val url: String,
override val lang: String,
val scanId: Long = 1,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
private val isCi = System.getenv("CI") == "true"
private val preferences: SharedPreferences = getPreferences()
protected var apiUrl: String
get() = preferences.getString(API_BASE_URL_PREF, defaultApiUrl)!!
private set(value) = preferences.edit().putString(API_BASE_URL_PREF, value).apply()
private var restoreDefaultEnable: Boolean
get() = preferences.getBoolean(DEFAULT_PREF, false)
set(value) = preferences.edit().putBoolean(DEFAULT_PREF, value).apply()
override val baseUrl: String get() = when {
isCi -> defaultBaseUrl
else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
}
private val defaultBaseUrl: String = url
private val defaultApiUrl: String = "https://api.sussytoons.wtf"
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageLocation)
.build()
init {
if (restoreDefaultEnable) {
restoreDefaultEnable = false
preferences.edit().putString(DEFAULT_BASE_URL_PREF, null).apply()
preferences.edit().putString(API_DEFAULT_BASE_URL_PREF, null).apply()
}
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
preferences.getString(API_DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultApiUrl) {
preferences.edit()
.putString(API_BASE_URL_PREF, defaultApiUrl)
.putString(API_DEFAULT_BASE_URL_PREF, defaultApiUrl)
.apply()
}
}
}
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", scanId.toString())
// ============================= Popular ==================================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaParse(response: Response): MangasPage {
val json = response.parseScriptToJson().let(POPULAR_JSON_REGEX::find)
?.groups?.get(1)?.value
?: return MangasPage(emptyList(), false)
val mangas = json.parseAs<ResultDto<List<MangaDto>>>().toSMangaList()
return MangasPage(mangas, false)
}
// ============================= Latest ===================================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.addQueryParameter("gen_id", "4")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
val mangas = dto.toSMangaList()
return MangasPage(mangas, dto.hasNextPage())
}
// ============================= Search ===================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("obr_nome", query)
.addQueryParameter("limite", "8")
.addQueryParameter("pagina", page.toString())
.addQueryParameter("todos_generos", "true")
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
return MangasPage(dto.toSMangaList(), dto.hasNextPage())
}
// ============================= Details ==================================
override fun mangaDetailsParse(response: Response): SManga {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?: throw IOException("Details do mangá não foi encontrado")
return json.parseAs<ResultDto<MangaDto>>().results.toSManga()
}
// ============================= Chapters =================================
override fun chapterListParse(response: Response): List<SChapter> {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?: return emptyList()
return json.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
}
// ============================= Pages ====================================
private val pageUrlSelector = "img.chakra-image"
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
pageListParse(document).takeIf(List<Page>::isNotEmpty)?.let { return it }
val dto = extractScriptData(document)
.let(::extractJsonContent)
.let(::parseJsonToChapterPageDto)
return dto.toPageList()
}
private fun pageListParse(document: Document): List<Page> {
return document.select(pageUrlSelector).mapIndexed { index, element ->
Page(index, document.location(), element.absUrl("src"))
}
}
private fun extractScriptData(document: Document): String {
return document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
}
private fun extractJsonContent(scriptData: String): String {
return pageRegex.find(scriptData)
?.groups?.get(1)?.value
?.let { "\"$it\"".parseAs<String>() }
?: throw Exception("Failed to extract JSON from script")
}
private fun parseJsonToChapterPageDto(jsonContent: String): ResultDto<ChapterPageDto> {
return try {
jsonContent.parseAs<ResultDto<ChapterPageDto>>()
} catch (e: Exception) {
throw Exception("Failed to load pages: ${e.message}")
}
}
override fun imageUrlParse(response: Response): String = ""
override fun imageUrlRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
return GET(page.url, imageHeaders)
}
// ============================= Interceptors =================================
private fun imageLocation(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful) {
return response
}
response.close()
val url = request.url.newBuilder()
.dropPathSegment(4)
.build()
val newRequest = request.newBuilder()
.url(url)
.build()
return chain.proceed(newRequest)
}
// ============================= Settings ====================================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val fields = listOf(
EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = URL_PREF_SUMMARY
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL padrão:\n$defaultBaseUrl"
setDefaultValue(defaultBaseUrl)
},
EditTextPreference(screen.context).apply {
key = API_BASE_URL_PREF
title = API_BASE_URL_PREF_TITLE
summary = buildString {
append("Se não souber como verificar a URL da API, ")
append("busque suporte no Discord do repositório de extensões.")
appendLine(URL_PREF_SUMMARY)
append("\n⚠ A fonte não oferece suporte para essa extensão.")
}
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL da API padrão:\n$defaultApiUrl"
setDefaultValue(defaultApiUrl)
},
SwitchPreferenceCompat(screen.context).apply {
key = DEFAULT_PREF
title = "Redefinir configurações"
summary = buildString {
append("Habilite para redefinir as configurações padrões no próximo reinicialização da aplicação.")
appendLine("Você pode limpar os dados da extensão em Configurações > Avançado:")
appendLine("\t - Limpar os cookies")
appendLine("\t - Limpar os dados da WebView")
appendLine("\t - Limpar o banco de dados (Procure a '$name' e remova os dados)")
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
},
)
fields.forEach(screen::addPreference)
}
// ============================= Utilities ====================================
private fun Response.parseScriptToJson(): String {
val document = asJsoup()
val script = document.select("script")
.map(Element::data)
.filter(String::isNotEmpty)
.joinToString("\n")
return QuickJs.create().use {
it.evaluate(
"""
globalThis.self = globalThis;
$script
self.__next_f.map(it => it[it.length - 1]).join('')
""".trimIndent(),
) as String
}
}
private fun HttpUrl.Builder.dropPathSegment(count: Int): HttpUrl.Builder {
repeat(count) {
removePathSegment(0)
}
return this
}
companion object {
const val CDN_URL = "https://cdn.sussytoons.site"
val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex()
val POPULAR_JSON_REGEX = """(?:"dataTop":)(\{.+totalPaginas":\d+\})(?:.+"dataF)""".toRegex()
val DETAILS_CHAPTER_REGEX = """(\{\"resultado.+"\}{3})""".toRegex()
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_TITLE = "Editar URL da fonte"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
private const val API_BASE_URL_PREF = "overrideApiUrl"
private const val API_BASE_URL_PREF_TITLE = "Editar URL da API da fonte"
private const val API_DEFAULT_BASE_URL_PREF = "defaultApiUrl"
private const val DEFAULT_PREF = "defaultPref"
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 33 baseVersionCode = 31

View File

@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -74,10 +73,10 @@ abstract class GroupLe(
override fun latestUpdatesSelector() = popularMangaSelector() override fun latestUpdatesSelector() = popularMangaSelector()
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${50 * (page - 1)}", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}", headers)
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/list?sortType=updated&offset=${50 * (page - 1)}", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}", headers)
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
@ -104,73 +103,15 @@ abstract class GroupLe(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search/advancedResults?offset=${50 * (page - 1)}" val url =
.toHttpUrl() "$baseUrl/search/advancedResults?offset=${50 * (page - 1)}".toHttpUrl()
.newBuilder() .newBuilder()
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
url.addQueryParameter("q", query) url.addQueryParameter("q", query)
} }
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is GenreList -> filter.state.forEach { genre ->
if (genre.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(genre.id, arrayOf("=", "=in", "=ex")[genre.state])
}
}
is CategoryList -> filter.state.forEach { category ->
if (category.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(category.id, arrayOf("=", "=in", "=ex")[category.state])
}
}
is AgeList -> filter.state.forEach { age ->
if (age.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(age.id, arrayOf("=", "=in", "=ex")[age.state])
}
}
is MoreList -> filter.state.forEach { more ->
if (more.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(more.id, arrayOf("=", "=in", "=ex")[more.state])
}
}
is AdditionalFilterList -> filter.state.forEach { fils ->
if (fils.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(fils.id, arrayOf("=", "=in", "=ex")[fils.state])
}
}
is OrderBy -> {
url.addQueryParameter(
"sortType",
arrayOf("RATING", "POPULARITY", "YEAR", "NAME", "DATE_CREATE", "DATE_UPDATE", "USER_RATING")[filter.state],
)
}
else -> {}
}
}
return GET(url.toString().replace("=%3D", "="), headers) return GET(url.toString().replace("=%3D", "="), headers)
} }
protected class OrderBy : Filter.Select<String>(
"Сортировка",
arrayOf("По популярности", "Популярно сейчас", "По году", "По алфавиту", "Новинки", "По дате обновления", "По рейтингу"),
)
protected class Genre(name: String, val id: String) : Filter.TriState(name)
protected class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанры", genres)
protected class CategoryList(categories: List<Genre>) : Filter.Group<Genre>("Категории", categories)
protected class AgeList(ages: List<Genre>) : Filter.Group<Genre>("Возрастная рекомендация", ages)
protected class MoreList(moren: List<Genre>) : Filter.Group<Genre>("Прочее", moren)
protected class AdditionalFilterList(fils: List<Genre>) : Filter.Group<Genre>("Фильтры", fils)
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select(".expandable").first()!! val infoElement = document.select(".expandable").first()!!
val rawCategory = infoElement.select("span.elem_category").text() val rawCategory = infoElement.select("span.elem_category").text()

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 9 baseVersionCode = 8

View File

@ -127,7 +127,7 @@ abstract class Iken(
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: "" val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
val id = response.request.url.fragment!! val id = response.request.url.fragment!!
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=900&order=desc&userid=$userId" val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute() val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
val data = chapterResponse.parseAs<Post<ChapterListResponse>>() val data = chapterResponse.parseAs<Post<ChapterListResponse>>()

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 21 baseVersionCode = 20

View File

@ -2,13 +2,11 @@ package eu.kanade.tachiyomi.multisrc.kemono
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.double import kotlinx.serialization.json.double
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@Serializable @Serializable
class KemonoFavouritesDto( class KemonoFavouritesDto(
val id: String, val id: String,
@ -27,7 +25,7 @@ class KemonoCreatorDto(
) { ) {
var fav: Long = 0 var fav: Long = 0
val updatedDate get() = when { val updatedDate get() = when {
updated.isString -> dateFormat.tryParse(updated.content) updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
else -> (updated.double * 1000).toLong() else -> (updated.double * 1000).toLong()
} }
@ -64,7 +62,7 @@ class KemonoPostDto(
private val service: String, private val service: String,
private val user: String, private val user: String,
private val title: String, private val title: String,
private val added: String?, private val added: String,
private val published: String?, private val published: String?,
private val edited: String?, private val edited: String?,
private val file: KemonoFileDto, private val file: KemonoFileDto,
@ -82,13 +80,13 @@ class KemonoPostDto(
}.distinctBy { it.path }.map { it.toString() } }.distinctBy { it.path }.map { it.toString() }
fun toSChapter() = SChapter.create().apply { fun toSChapter() = SChapter.create().apply {
val postDate = dateFormat.tryParse(edited ?: published ?: added) val postDate = dateFormat.parse(edited ?: published ?: added)
url = "/$service/user/$user/post/$id" url = "/$service/user/$user/post/$id"
date_upload = postDate date_upload = postDate?.time ?: 0
name = title.ifBlank { name = title.ifBlank {
val postDateString = when { val postDateString = when {
postDate != 0L -> chapterNameDateFormat.format(postDate) postDate != null && postDate.time != 0L -> chapterNameDateFormat.format(postDate)
else -> "unknown date" else -> "unknown date"
} }

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 17 baseVersionCode = 15
dependencies { dependencies {
api(project(":lib:i18n")) api(project(":lib:i18n"))

View File

@ -59,16 +59,8 @@ abstract class Keyoapp(
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers) override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
open val popularMangaTitleSelector = listOf( override fun popularMangaSelector(): String =
"Popular", "div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
"Popularie",
"Trending",
)
override fun popularMangaSelector(): String = selector(
"div:contains(%s) + div .group.overflow-hidden.grid",
popularMangaTitleSelector,
)
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.getImageUrl("*[style*=background-image]") thumbnail_url = element.getImageUrl("*[style*=background-image]")
@ -251,7 +243,7 @@ abstract class Keyoapp(
override fun chapterListSelector(): String { override fun chapterListSelector(): String {
if (!preferences.showPaidChapters) { if (!preferences.showPaidChapters) {
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[alt~=Coin]))" return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[src*=Coin.svg]))"
} }
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))" return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
} }
@ -365,10 +357,6 @@ abstract class Keyoapp(
return now.timeInMillis return now.timeInMillis
} }
private fun selector(selector: String, contains: List<String>): String {
return contains.joinToString { selector.replace("%s", it) }
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = SHOW_PAID_CHAPTERS_PREF key = SHOW_PAID_CHAPTERS_PREF

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 42 baseVersionCode = 41
dependencies { dependencies {
api(project(":lib:cryptoaes")) api(project(":lib:cryptoaes"))

View File

@ -245,13 +245,11 @@ abstract class Madara(
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply { val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(mangaSubString) addPathSegment(mangaSubString)
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX)) addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
addPathSegment("") // add trailing slash
}.build() }.build()
return client.newCall(GET(mangaUrl, headers)) return client.newCall(GET(mangaUrl, headers))
.asObservableSuccess().map { response -> .asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply { val manga = mangaDetailsParse(response).apply {
setUrlWithoutDomain(mangaUrl.toString()) setUrlWithoutDomain(mangaUrl.toString())
initialized = true
} }
MangasPage(listOf(manga), false) MangasPage(listOf(manga), false)
@ -979,8 +977,6 @@ abstract class Madara(
open val pageListParseSelector = "div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img" open val pageListParseSelector = "div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img"
open val chapterProtectorSelector = "#chapter-protector-data" open val chapterProtectorSelector = "#chapter-protector-data"
open val chapterProtectorPasswordPrefix = "wpmangaprotectornonce='"
open val chapterProtectorDataPrefix = "chapter_data='"
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
launchIO { countViews(document) } launchIO { countViews(document) }
@ -996,11 +992,11 @@ abstract class Madara(
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) } ?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
?: chapterProtector.html() ?: chapterProtector.html()
val password = chapterProtectorHtml val password = chapterProtectorHtml
.substringAfter(chapterProtectorPasswordPrefix) .substringAfter("wpmangaprotectornonce='")
.substringBefore("';") .substringBefore("';")
val chapterData = json.parseToJsonElement( val chapterData = json.parseToJsonElement(
chapterProtectorHtml chapterProtectorHtml
.substringAfter(chapterProtectorDataPrefix) .substringAfter("chapter_data='")
.substringBefore("';") .substringBefore("';")
.replace("\\/", "/"), .replace("\\/", "/"),
).jsonObject ).jsonObject

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 7 baseVersionCode = 6

View File

@ -85,9 +85,6 @@ abstract class MangaBox(
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response { private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
if (cdnSet.isEmpty()) {
return chain.proceed(request)
}
val requestTag = request.tag(MangaBoxFallBackTag::class.java) val requestTag = request.tag(MangaBoxFallBackTag::class.java)
val originalResponse: Response? = try { val originalResponse: Response? = try {
chain.proceed(request) chain.proceed(request)
@ -349,10 +346,11 @@ abstract class MangaBox(
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val content = document.select("script:containsData(cdns =)").joinToString("\n") { it.data() } val element = document.select("head > script").lastOrNull()
?: return emptyList()
val cdns = val cdns =
extractArray(content, "cdns") + extractArray(content, "backupImage") extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
val chapterImages = extractArray(content, "chapterImages") val chapterImages = extractArray(element.html(), "chapterImages")
// Add all parsed cdns to set // Add all parsed cdns to set
cdnSet.addAll(cdns) cdnSet.addAll(cdns)
@ -371,10 +369,6 @@ abstract class MangaBox(
} }
Page(i, document.location(), parsedUrl) Page(i, document.location(), parsedUrl)
}.ifEmpty {
document.select("div.container-chapter-reader > img").mapIndexed { i, img ->
Page(i, imageUrl = img.absUrl("src"))
}
} }
} }

View File

@ -2,9 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 34 baseVersionCode = 30
dependencies { dependencies {
//noinspection UseTomlInstead api(project(":lib:randomua"))
implementation("org.brotli:dec:0.1.2")
} }

View File

@ -1,71 +1,62 @@
package eu.kanade.tachiyomi.multisrc.mangahub package eu.kanade.tachiyomi.multisrc.mangahub
import android.content.SharedPreferences import eu.kanade.tachiyomi.lib.randomua.UserAgentType
import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import keiyoushi.utils.getPreferencesLazy import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs import kotlinx.serialization.decodeFromString
import keiyoushi.utils.tryParse import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import org.jsoup.nodes.Document
import org.brotli.dec.BrotliInputStream import org.jsoup.nodes.Element
import java.io.ByteArrayOutputStream import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.URLEncoder import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.GZIPInputStream
import kotlin.random.Random
abstract class MangaHub( abstract class MangaHub(
override val name: String, override val name: String,
final override val baseUrl: String, final override val baseUrl: String,
override val lang: String, override val lang: String,
private val mangaSource: String, private val mangaSource: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH), private val dateFormat: SimpleDateFormat = SimpleDateFormat("MM-dd-yyyy", Locale.US),
) : HttpSource(), ConfigurableSource { ) : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
private val baseApiUrl = "https://api.mghcdn.com" private var baseApiUrl = "https://api.mghcdn.com"
private val baseCdnUrl = "https://imgx.mghcdn.com" private var baseCdnUrl = "https://imgx.mghcdn.com"
private val baseThumbCdnUrl = "https://thumb.mghcdn.com" private val regex = Regex("mhub_access=([^;]+)")
private val apiRegex = Regex("mhub_access=([^;]+)")
private val spaceRegex = Regex("\\s+")
private val apiErrorRegex = Regex("""rate\s*limit|api\s*key""")
private val preferences: SharedPreferences by getPreferencesLazy()
private fun SharedPreferences.getUseGenericTitlePref(): Boolean = getBoolean(
PREF_USE_GENERIC_TITLE,
false,
)
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
userAgentType = UserAgentType.DESKTOP,
filterInclude = listOf("chrome"),
)
.addInterceptor(::apiAuthInterceptor) .addInterceptor(::apiAuthInterceptor)
.addNetworkInterceptor(::compatEncodingInterceptor) .rateLimit(1)
.build() .build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder() override fun headersBuilder(): Headers.Builder = super.headersBuilder()
@ -78,158 +69,60 @@ abstract class MangaHub(
.add("Sec-Fetch-Site", "same-origin") .add("Sec-Fetch-Site", "same-origin")
.add("Upgrade-Insecure-Requests", "1") .add("Upgrade-Insecure-Requests", "1")
private fun postRequestGraphQL(query: String, refreshUrl: String? = null): Request { open val json: Json by injectLazy()
val requestHeaders = headersBuilder()
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.set("Origin", baseUrl)
.set("Sec-Fetch-Dest", "empty")
.set("Sec-Fetch-Mode", "cors")
.set("Sec-Fetch-Site", "cross-site")
.removeAll("Upgrade-Insecure-Requests")
.build()
val body = buildJsonObject {
put("query", query)
}
return POST("$baseApiUrl/graphql", requestHeaders, body.toString().toRequestBody())
.newBuilder()
.tag(GraphQLTag::class.java, GraphQLTag(refreshUrl))
.build()
}
// Normally this gets handled properly but in older forks such as TachiyomiJ2K, we have to manually intercept it
// as they have an outdated implementation of NetworkHelper.
private fun compatEncodingInterceptor(chain: Interceptor.Chain): Response {
var response = chain.proceed(chain.request())
val contentEncoding = response.header("Content-Encoding")
if (contentEncoding == "gzip") {
val parsedBody = response.body.byteStream().let { gzipInputStream ->
GZIPInputStream(gzipInputStream).use { inputStream ->
val outputStream = ByteArrayOutputStream()
inputStream.copyTo(outputStream)
outputStream.toByteArray()
}
}
response = response.createNewWithCompatBody(parsedBody)
} else if (contentEncoding == "br") {
val parsedBody = response.body.byteStream().let { brotliInputStream ->
BrotliInputStream(brotliInputStream).use { inputStream ->
val outputStream = ByteArrayOutputStream()
inputStream.copyTo(outputStream)
outputStream.toByteArray()
}
}
response = response.createNewWithCompatBody(parsedBody)
}
return response
}
private fun Response.createNewWithCompatBody(outputStream: ByteArray): Response {
return this.newBuilder()
.body(outputStream.toResponseBody(this.body.contentType()))
.removeHeader("Content-Encoding")
.build()
}
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response { private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request() val originalRequest = chain.request()
val tag = request.tag(GraphQLTag::class.java)
?: return chain.proceed(request) // We won't intercept non-graphql requests (like image retrieval)
return try {
tryApiRequest(chain, request)
} catch (e: Throwable) {
val noCookie = e is MangaHubCookieNotFound
val apiError = e is ApiErrorException &&
apiErrorRegex.containsMatchIn(e.message ?: "")
if (noCookie || apiError) {
refreshApiKey(tag.refreshUrl)
tryApiRequest(chain, request)
} else {
throw e
}
}
}
private fun tryApiRequest(chain: Interceptor.Chain, request: Request): Response {
val cookie = client.cookieJar val cookie = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl()) .loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() } .firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
?: throw MangaHubCookieNotFound()
val apiRequest = request.newBuilder() val request =
.header("x-mhub-access", cookie.value) if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
.build() originalRequest.newBuilder()
.header("x-mhub-access", cookie.value)
.build()
} else {
originalRequest
}
val response = chain.proceed(apiRequest) return chain.proceed(request)
val apiResponse = response.peekBody(Long.MAX_VALUE).string()
.parseAs<ApiResponseError>()
if (apiResponse.errors != null) {
response.close() // Avoid leaks
val errors = apiResponse.errors.joinToString("\n") { it.message }
throw ApiErrorException(errors)
}
return response
} }
private class MangaHubCookieNotFound : IOException("mhub_access cookie not found") private fun refreshApiKey(chapter: SChapter) {
private class ApiErrorException(errorMessage: String) : IOException(errorMessage) val slug = "$baseUrl${chapter.url}"
.toHttpUrlOrNull()
?.pathSegments
?.get(1)
private val lock = ReentrantLock() val url = if (slug != null) {
private var refreshed = 0L "$baseUrl/manga/$slug".toHttpUrl()
private fun refreshApiKey(refreshUrl: String? = null) {
if (refreshed + 10000 < System.currentTimeMillis() && lock.tryLock()) {
val url = when {
refreshUrl != null -> refreshUrl
else -> "$baseUrl/chapter/martial-peak/chapter-${Random.nextInt(1000, 3000)}"
}.toHttpUrl()
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
try {
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
val response = client.newCall(
GET(
"$url$query",
headers.newBuilder()
.set("Referer", "$baseUrl/manga/${url.pathSegments[1]}")
.build(),
),
).execute()
val returnedKey =
response.headers["set-cookie"]?.let { apiRegex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
if (returnedKey != oldKey) break // Break out of loop since we got an allegedly valid API key
} catch (_: Throwable) {
lock.unlock()
throw Exception("An error occurred while obtaining a new API key") // Show error
}
}
refreshed = System.currentTimeMillis()
lock.unlock()
} else { } else {
lock.lock() // wait here until lock is released baseUrl.toHttpUrl()
lock.unlock() }
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
try {
val response = client.newCall(GET("$url$query", headers)).execute()
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
} catch (_: IOException) {
throw IOException("An error occurred while obtaining a new API key") // Show error
}
} }
} }
@ -240,36 +133,35 @@ abstract class MangaHub(
val signature: String, val signature: String,
) )
private fun ApiMangaSearchItem.toSignature(): String { private fun Element.toSignature(): String {
val author = this.author val author = this.select("small").text()
val chNum = this.latestChapter val chNum = this.select(".col-sm-6 a:contains(#)").text()
val genres = this.genres val genres = this.select(".genre-label").joinToString { it.text() }
return author + chNum + genres return author + chNum + genres
} }
private fun mangaRequest(page: Int, order: String): Request {
return postRequestGraphQL(searchQuery(mangaSource, "", "all", order, page))
}
// popular // popular
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, "POPULAR") override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/popular/page/$page", headers)
}
// often enough there will be nearly identical entries with slightly different // often enough there will be nearly identical entries with slightly different
// titles, URLs, and image names. in order to cut these "duplicates" down, // titles, URLs, and image names. in order to cut these "duplicates" down,
// assign a "signature" based on author name, chapter number, and genres // assign a "signature" based on author name, chapter number, and genres
// if all of those are the same, then it it's the same manga // if all of those are the same, then it it's the same manga
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val mangaList = response.parseAs<ApiSearchResponse>() val doc = response.asJsoup()
val mangas = mangaList.data.search.rows.map { val mangas = doc.select(popularMangaSelector())
SMangaDTO( .map {
"$baseUrl/manga/${it.slug}", SMangaDTO(
it.title, it.select("h4 a").attr("abs:href"),
"$baseThumbCdnUrl/${it.image}", it.select("h4 a").text(),
it.toSignature(), it.select("img").attr("abs:src"),
) it.toSignature(),
} )
}
.distinctBy { it.signature } .distinctBy { it.signature }
.map { .map {
SManga.create().apply { SManga.create().apply {
@ -278,171 +170,221 @@ abstract class MangaHub(
thumbnail_url = it.thumbnailUrl thumbnail_url = it.thumbnailUrl
} }
} }
return MangasPage(mangas, doc.select(popularMangaNextPageSelector()).isNotEmpty())
// Entries have a max of 30 per request
return MangasPage(mangas, mangaList.data.search.rows.count() == 30)
} }
override fun popularMangaSelector() = ".col-sm-6:not(:has(a:contains(Yaoi)))"
override fun popularMangaFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun popularMangaNextPageSelector() = "ul.pager li.next > a"
// latest // latest
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return mangaRequest(page, "LATEST") return GET("$baseUrl/updates/page/$page", headers)
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
return popularMangaParse(response) return popularMangaParse(response)
} }
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// search // search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var order = "POPULAR" val url = "$baseUrl/search/page/$page".toHttpUrl().newBuilder()
var genres = "all" url.addQueryParameter("q", query)
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) { when (filter) {
is OrderBy -> { is OrderBy -> {
order = filter.values[filter.state].key val order = filter.values[filter.state]
url.addQueryParameter("order", order.key)
} }
is GenreList -> { is GenreList -> {
genres = filter.included.joinToString(",").takeIf { it.isNotBlank() } ?: "all" val genre = filter.values[filter.state]
url.addQueryParameter("genre", genre.key)
} }
else -> {} else -> {}
} }
} }
return GET(url.build(), headers)
}
return postRequestGraphQL(searchQuery(mangaSource, query, genres, order, page)) override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga {
throw UnsupportedOperationException()
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
return popularMangaParse(response) return popularMangaParse(response)
} }
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// manga details // manga details
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsParse(document: Document): SManga {
return postRequestGraphQL( val manga = SManga.create()
mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")), manga.title = document.select(".breadcrumb .active span").text()
refreshUrl = "$baseUrl${manga.url}", manga.author = document.select("div:has(h1) span:contains(Author) + span").first()?.text()
) manga.artist = document.select("div:has(h1) span:contains(Artist) + span").first()?.text()
} manga.genre = document.select(".row p a").joinToString { it.text() }
manga.description = document.select(".tab-content p").first()?.text()
manga.thumbnail_url = document.select("img.img-responsive").first()
?.attr("src")
override fun mangaDetailsParse(response: Response): SManga { document.select("div:has(h1) span:contains(Status) + span").first()?.text()?.also { statusText ->
val rawManga = response.parseAs<ApiMangaDetailsResponse>() when {
statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
return SManga.create().apply { statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
title = rawManga.data.manga.title!! else -> manga.status = SManga.UNKNOWN
author = rawManga.data.manga.author
artist = rawManga.data.manga.artist
genre = rawManga.data.manga.genres
thumbnail_url = "$baseThumbCdnUrl/${rawManga.data.manga.image}"
status = when (rawManga.data.manga.status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
} }
}
description = buildString { // add alternative name to manga description
rawManga.data.manga.description?.let(::append) document.select("h1 small").firstOrNull()?.ownText()?.let { alternativeName ->
if (alternativeName.isNotBlank()) {
// Add alternative title manga.description = manga.description.orEmpty().let {
val altTitle = rawManga.data.manga.alternativeTitle if (it.isBlank()) {
if (!altTitle.isNullOrBlank()) { "Alternative Name: $alternativeName"
if (isNotBlank()) append("\n\n") } else {
append("Alternative Name: $altTitle") "$it\n\nAlternative Name: $alternativeName"
}
} }
} }
} }
return manga
} }
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}" // chapters
// Chapters
override fun chapterListRequest(manga: SManga): Request {
return postRequestGraphQL(
mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")),
refreshUrl = "$baseUrl${manga.url}",
)
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = response.parseAs<ApiMangaDetailsResponse>() val document = response.asJsoup()
val useGenericTitle = preferences.getUseGenericTitlePref() val head = document.head()
return document.select(chapterListSelector()).map { chapterFromElement(it, head) }
return chapterList.data.manga.chapters!!.map {
SChapter.create().apply {
val numberString = "${if (it.number % 1 == 0f) it.number.toInt() else it.number}"
name = if (!useGenericTitle) {
generateChapterName(it.title.trim().replace(spaceRegex, " "), numberString)
} else {
generateGenericChapterName(numberString)
}
url = "/${chapterList.data.manga.slug}/chapter-${it.number}"
chapter_number = it.number
date_upload = dateFormat.tryParse(it.date)
}
}.reversed() // The response is sorted in ASC format so we need to reverse it
} }
private fun generateChapterName(title: String, number: String): String { override fun chapterListSelector() = ".tab-content ul li"
return if (title.contains(number)) {
title private fun chapterFromElement(element: Element, head: Element): SChapter {
} else if (title.isNotBlank()) { val chapter = SChapter.create()
"Chapter $number - $title" val potentialLinks = element.select("a[href*='$baseUrl/chapter/']:not([rel*=nofollow]):not([rel*=noreferrer])")
} else { var visibleLink = ""
generateGenericChapterName(number) potentialLinks.forEach { a ->
val className = a.className()
val styles = head.select("style").html()
if (!styles.contains(".$className { display:none; }")) {
visibleLink = a.attr("href")
return@forEach
}
} }
chapter.setUrlWithoutDomain(visibleLink)
chapter.name = chapter.url.trimEnd('/').substringAfterLast('/').replace('-', ' ')
chapter.date_upload = element.select("small.UovLc").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun generateGenericChapterName(number: String): String { override fun chapterFromElement(element: Element): SChapter {
return "Chapter $number" throw UnsupportedOperationException()
} }
override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/chapter${chapter.url}" private fun parseChapterDate(date: String): Long {
val now = Calendar.getInstance().apply {
// Pages set(Calendar.HOUR_OF_DAY, 0)
override fun pageListRequest(chapter: SChapter): Request { set(Calendar.MINUTE, 0)
val chapterUrl = chapter.url.split("/") set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
return postRequestGraphQL( }
pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()), var parsedDate = 0L
refreshUrl = "$baseUrl/chapter${chapter.url}", when {
) "just now" in date || "less than an hour" in date -> {
} parsedDate = now.timeInMillis
override fun pageListParse(response: Response): List<Page> {
val chapterObject = response.parseAs<ApiChapterPagesResponse>()
val pages = chapterObject.data.chapter.pages.parseAs<ApiChapterPages>()
// We'll update the cookie here to match the browser's "recently" opened chapter.
// This mimics how the browser works and gives us more chance to receive a valid API key upon refresh
val now = Calendar.getInstance().time.time
val baseHttpUrl = baseUrl.toHttpUrl()
val recently = buildJsonObject {
putJsonObject((now).toString()) {
put("mangaID", chapterObject.data.chapter.mangaID)
put("number", chapterObject.data.chapter.chapterNumber)
} }
}.toString() // parses: "1 hour ago" and "2 hours ago"
"hour" in date -> {
val hours = date.replaceAfter(" ", "").trim().toInt()
parsedDate = now.apply { add(Calendar.HOUR, -hours) }.timeInMillis
}
// parses: "Yesterday" and "2 days ago"
"day" in date -> {
val days = date.replace("days ago", "").trim().toIntOrNull() ?: 1
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -days) }.timeInMillis
}
// parses: "2 weeks ago"
"weeks" in date -> {
val weeks = date.replace("weeks ago", "").trim().toInt()
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -weeks) }.timeInMillis
}
// parses: "12-20-2019" and defaults everything that wasn't taken into account to 0
else -> {
try {
parsedDate = dateFormat.parse(date)?.time ?: 0L
} catch (e: ParseException) { /*nothing to do, parsedDate is initialized with 0L*/ }
}
}
return parsedDate
}
val recentlyCookie = Cookie.Builder() // pages
.domain(baseHttpUrl.host) override fun pageListRequest(chapter: SChapter): Request {
.name("recently") val body = buildJsonObject {
.value(URLEncoder.encode(recently, "utf-8")) put("query", PAGES_QUERY)
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months put(
"variables",
buildJsonObject {
val chapterUrl = chapter.url.split("/")
put("mangaSource", mangaSource)
put("slug", chapterUrl[2])
put("number", chapterUrl[3].substringAfter("-").toFloat())
},
)
}
.toString()
.toRequestBody()
val newHeaders = headersBuilder()
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.set("Origin", baseUrl)
.set("Sec-Fetch-Dest", "empty")
.set("Sec-Fetch-Mode", "cors")
.set("Sec-Fetch-Site", "cross-site")
.removeAll("Upgrade-Insecure-Requests")
.build() .build()
// Add/update the cookie return POST("$baseApiUrl/graphql", newHeaders, body)
client.cookieJar.saveFromResponse(baseHttpUrl, listOf(recentlyCookie)) }
// We'll log our action to the site to further increase the chance of valid API key override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
val ipRequest = client.newCall(GET("https://api.ipify.org?format=json")).execute() super.fetchPageList(chapter)
val ip = ipRequest.parseAs<PublicIPResponse>().ip .doOnError { refreshApiKey(chapter) }
.retry(1)
client.newCall(GET("$baseUrl/action/logHistory2/${chapterObject.data.chapter.manga.slug}/${chapterObject.data.chapter.chapterNumber}?browserID=$ip")).execute().close() override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
ipRequest.close() override fun pageListParse(response: Response): List<Page> {
val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
return pages.images.mapIndexed { i, page -> if (chapterObject.data?.chapter == null) {
Page(i, "", "$baseCdnUrl/${pages.page}$page") if (chapterObject.errors != null) {
val errors = chapterObject.errors.joinToString("\n") { it.message }
throw Exception(errors)
}
throw Exception("Unknown error while processing pages")
}
val pages = json.decodeFromString<ApiChapterPages>(chapterObject.data.chapter.pages)
return pages.i.mapIndexed { i, page ->
Page(i, "", "$baseCdnUrl/${pages.p}$page")
} }
} }
@ -459,14 +401,10 @@ abstract class MangaHub(
return GET(page.url, newHeaders) return GET(page.url, newHeaders)
} }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// filters // filters
private class Genre(title: String, val key: String) : Filter.CheckBox(title) { private class Genre(title: String, val key: String) : Filter.TriState(title) {
fun getGenreKey(): String {
return key
}
override fun toString(): String { override fun toString(): String {
return name return name
} }
@ -479,14 +417,11 @@ abstract class MangaHub(
} }
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0) private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) { private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Genres", genres, 0)
val included: List<String>
get() = state.filter { it.state }.map { it.getGenreKey() }
}
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
GenreList(genres),
OrderBy(orderBy), OrderBy(orderBy),
GenreList(genres),
) )
private val orderBy = arrayOf( private val orderBy = arrayOf(
@ -497,119 +432,70 @@ abstract class MangaHub(
Order("Completed", "COMPLETED"), Order("Completed", "COMPLETED"),
) )
private val genres = listOf( private val genres = arrayOf(
Genre("All Genres", "all"),
Genre("[no chapters]", "no-chapters"),
Genre("4-Koma", "4-koma"),
Genre("Action", "action"), Genre("Action", "action"),
Genre("Adventure", "adventure"), Genre("Adventure", "adventure"),
Genre("Award Winning", "award-winning"),
Genre("Comedy", "comedy"), Genre("Comedy", "comedy"),
Genre("Adult", "adult"),
Genre("Drama", "drama"),
Genre("Historical", "historical"),
Genre("Martial Arts", "martial-arts"),
Genre("Romance", "romance"),
Genre("Ecchi", "ecchi"),
Genre("Supernatural", "supernatural"),
Genre("Webtoons", "webtoons"),
Genre("Manhwa", "manhwa"),
Genre("Fantasy", "fantasy"),
Genre("Harem", "harem"),
Genre("Shounen", "shounen"),
Genre("Manhua", "manhua"),
Genre("Mature", "mature"),
Genre("Seinen", "seinen"),
Genre("Sports", "sports"),
Genre("School Life", "school-life"),
Genre("Smut", "smut"),
Genre("Mystery", "mystery"),
Genre("Psychological", "psychological"),
Genre("Shounen ai", "shounen-ai"),
Genre("Slice of life", "slice-of-life"),
Genre("Shoujo ai", "shoujo-ai"),
Genre("Cooking", "cooking"), Genre("Cooking", "cooking"),
Genre("Horror", "horror"), Genre("Crime", "crime"),
Genre("Tragedy", "tragedy"), Genre("Demons", "demons"),
Genre("Doujinshi", "doujinshi"), Genre("Doujinshi", "doujinshi"),
Genre("Sci-Fi", "sci-fi"), Genre("Drama", "drama"),
Genre("Yuri", "yuri"), Genre("Ecchi", "ecchi"),
Genre("Yaoi", "yaoi"), Genre("Fantasy", "fantasy"),
Genre("Shoujo", "shoujo"), Genre("Food", "food"),
Genre("Game", "game"),
Genre("Gender bender", "gender-bender"), Genre("Gender bender", "gender-bender"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Isekai", "isekai"),
Genre("Josei", "josei"), Genre("Josei", "josei"),
Genre("Kids", "kids"),
Genre("Magic", "magic"),
Genre("Magical Girls", "magical-girls"),
Genre("Manhua", "manhua"),
Genre("Manhwa", "manhwa"),
Genre("Martial arts", "martial-arts"),
Genre("Mature", "mature"),
Genre("Mecha", "mecha"), Genre("Mecha", "mecha"),
Genre("Medical", "medical"), Genre("Medical", "medical"),
Genre("Magic", "magic"),
Genre("4-Koma", "4-koma"),
Genre("Music", "music"),
Genre("Webtoon", "webtoon"),
Genre("Isekai", "isekai"),
Genre("Game", "game"),
Genre("Award Winning", "award-winning"),
Genre("Oneshot", "oneshot"),
Genre("Demons", "demons"),
Genre("Military", "military"), Genre("Military", "military"),
Genre("Music", "music"),
Genre("Mystery", "mystery"),
Genre("One shot", "one-shot"),
Genre("Oneshot", "oneshot"),
Genre("Parody", "parody"),
Genre("Police", "police"), Genre("Police", "police"),
Genre("Psychological", "psychological"),
Genre("Romance", "romance"),
Genre("School life", "school-life"),
Genre("Sci fi", "sci-fi"),
Genre("Seinen", "seinen"),
Genre("Shotacon", "shotacon"),
Genre("Shoujo", "shoujo"),
Genre("Shoujo ai", "shoujo-ai"),
Genre("Shoujoai", "shoujoai"),
Genre("Shounen", "shounen"),
Genre("Shounen ai", "shounen-ai"),
Genre("Shounenai", "shounenai"),
Genre("Slice of life", "slice-of-life"),
Genre("Smut", "smut"),
Genre("Space", "space"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"), Genre("Super Power", "super-power"),
Genre("Food", "food"),
Genre("Kids", "kids"),
Genre("Magical Girls", "magical-girls"),
Genre("Wuxia", "wuxia"),
Genre("Superhero", "superhero"), Genre("Superhero", "superhero"),
Genre("Supernatural", "supernatural"),
Genre("Thriller", "thriller"), Genre("Thriller", "thriller"),
Genre("Crime", "crime"), Genre("Tragedy", "tragedy"),
Genre("Philosophical", "philosophical"), Genre("Vampire", "vampire"),
Genre("Adaptation", "adaptation"), Genre("Webtoon", "webtoon"),
Genre("Full Color", "full-color"), Genre("Webtoons", "webtoons"),
Genre("Crossdressing", "crossdressing"), Genre("Wuxia", "wuxia"),
Genre("Reincarnation", "reincarnation"), Genre("Yuri", "yuri"),
Genre("Manga", "manga"), )
Genre("Cartoon", "cartoon"),
Genre("Survival", "survival"),
Genre("Comic", "comic"),
Genre("English", "english"),
Genre("Harlequin", "harlequin"),
Genre("Time Travel", "time-travel"),
Genre("Traditional Games", "traditional-games"),
Genre("Reverse Harem", "reverse-harem"),
Genre("Animals", "animals"),
Genre("Aliens", "aliens"),
Genre("Loli", "loli"),
Genre("Video Games", "video-games"),
Genre("Monsters", "monsters"),
Genre("Office Workers", "office-workers"),
Genre("System", "system"),
Genre("Villainess", "villainess"),
Genre("Zombies", "zombies"),
Genre("Vampires", "vampires"),
Genre("Violence", "violence"),
Genre("Monster Girls", "monster-girls"),
Genre("Anthology", "anthology"),
Genre("Ghosts", "ghosts"),
Genre("Delinquents", "delinquents"),
Genre("Post-Apocalyptic", "post-apocalyptic"),
Genre("Xianxia", "xianxia"),
Genre("Xuanhuan", "xuanhuan"),
Genre("R-18", "r-18"),
Genre("Cultivation", "cultivation"),
Genre("Rebirth", "rebirth"),
Genre("Gore", "gore"),
Genre("Russian", "russian"),
Genre("Samurai", "samurai"),
Genre("Ninja", "ninja"),
Genre("Revenge", "revenge"),
Genre("Cheat Systems", "cheat-systems"),
Genre("Dungeons", "dungeons"),
Genre("Overpowered", "overpowered"),
).sortedBy { it.toString() }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_USE_GENERIC_TITLE
title = "Use generic title"
summary = "Use generic chapter title (\"Chapter 'x'\") instead of the given one.\nNote: May require manga entry to be refreshed."
setDefaultValue(false)
}.let(screen::addPreference)
}
companion object {
private const val PREF_USE_GENERIC_TITLE = "pref_use_generic_title"
}
} }

View File

@ -1,97 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mangahub
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias ApiChapterPagesResponse = ApiResponse<ApiChapterData>
typealias ApiSearchResponse = ApiResponse<ApiSearchObject>
typealias ApiMangaDetailsResponse = ApiResponse<ApiMangaObject>
// Base classes
@Serializable
class ApiResponse<T>(
val data: T,
)
@Serializable
class ApiResponseError(
val errors: List<ApiErrorMessages>?,
)
@Serializable
class ApiErrorMessages(
val message: String,
)
@Serializable
class PublicIPResponse(
val ip: String,
)
// Chapter metadata (pages)
@Serializable
class ApiChapterData(
val chapter: ApiChapter,
)
@Serializable
class ApiChapter(
val pages: String,
val mangaID: Int,
@SerialName("number") val chapterNumber: Float,
val manga: ApiMangaData,
)
@Serializable
class ApiChapterPages(
@SerialName("p") val page: String,
@SerialName("i") val images: List<String>,
)
// Search, Popular, Latest
@Serializable
class ApiSearchObject(
val search: ApiSearchResults,
)
@Serializable
class ApiSearchResults(
val rows: List<ApiMangaSearchItem>,
)
@Serializable
class ApiMangaSearchItem(
val title: String,
val slug: String,
val image: String,
val author: String,
val latestChapter: Float,
val genres: String,
)
// Manga Details, Chapters
@Serializable
class ApiMangaObject(
val manga: ApiMangaData,
)
@Serializable
class ApiMangaData(
val title: String?,
val status: String?,
val image: String?,
val author: String?,
val artist: String?,
val genres: String?,
val description: String?,
val alternativeTitle: String?,
val slug: String?,
val chapters: List<ApiMangaChapterList>?,
)
@Serializable
class ApiMangaChapterList(
val number: Float,
val title: String,
val date: String,
)

View File

@ -1,70 +1,42 @@
package eu.kanade.tachiyomi.multisrc.mangahub package eu.kanade.tachiyomi.multisrc.mangahub
class GraphQLTag( import kotlinx.serialization.Serializable
val refreshUrl: String? = null,
private fun buildQuery(queryAction: () -> String) = queryAction().replace("%", "$")
val PAGES_QUERY = buildQuery {
"""
query(%mangaSource: MangaSource, %slug: String!, %number: Float!) {
chapter(x: %mangaSource, slug: %slug, number: %number) {
pages
}
}
""".trimIndent()
}
@Serializable
data class ApiErrorMessages(
val message: String,
) )
val searchQuery = { mangaSource: String, query: String, genre: String, order: String, page: Int -> @Serializable
""" data class ApiChapterPagesResponse(
{ val data: ApiChapterData?,
search(x: $mangaSource, q: "$query", genre: "$genre", mod: $order, offset: ${(page - 1) * 30}) { val errors: List<ApiErrorMessages>?,
rows { )
title,
author,
slug,
image,
genres,
latestChapter
}
}
}
""".trimIndent()
}
val mangaDetailsQuery = { mangaSource: String, slug: String -> @Serializable
""" data class ApiChapterData(
{ val chapter: ApiChapter?,
manga(x: $mangaSource, slug: "$slug") { )
title,
slug,
status,
image,
author,
artist,
genres,
description,
alternativeTitle
}
}
""".trimIndent()
}
val mangaChapterListQuery = { mangaSource: String, slug: String -> @Serializable
""" data class ApiChapter(
{ val pages: String,
manga(x: $mangaSource, slug: "$slug") { )
slug,
chapters {
number,
title,
date
}
}
}
""".trimIndent()
}
val pagesQuery = { mangaSource: String, slug: String, number: Float -> @Serializable
""" data class ApiChapterPages(
{ val p: String,
chapter(x: $mangaSource, slug: "$slug", number: $number) { val i: List<String>,
pages, )
mangaID,
number,
manga {
slug
}
}
}
""".trimIndent()
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 4 baseVersionCode = 3

View File

@ -14,12 +14,6 @@ class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
val response = chain.proceed(request) val response = chain.proceed(request)
val contentType = response.header("content-type")
if (contentType != null && contentType.startsWith("image/", ignoreCase = true)) {
return response
}
// ignore requests that already have completed the JS challenge // ignore requests that already have completed the JS challenge
if (response.headers["vary"] != null) return response if (response.headers["vary"] != null) return response

View File

@ -1,8 +0,0 @@
ext {
extName = 'BaoBua'
extClass = '.BaoBua'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,109 +0,0 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.UpdateStrategy
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class BaoBua() : SimpleParsedHttpSource() {
override val baseUrl = "https://www.baobua.net"
override val lang = "all"
override val name = "BaoBua"
override val supportsLatest = false
override fun simpleMangaSelector() = "article.post"
override fun simpleMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.popunder")!!.absUrl("href"))
title = element.selectFirst("div.read-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
override fun simpleNextPageSelector(): String = "nav.pagination a.next"
// region popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl?page=$page", headers)
// endregion
// region latest
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
// endregion
// region Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filter = filters.firstInstance<SourceCategorySelector>()
return filter.selectedCategory?.let {
GET(it.buildUrl(baseUrl), headers)
} ?: run {
baseUrl.toHttpUrl().newBuilder()
.addEncodedQueryParameter("q", query)
.addEncodedQueryParameter("page", page.toString())
.build()
.let { GET(it, headers) }
}
}
// region Details
override fun mangaDetailsParse(document: Document): SManga {
val trailItemsEl = document.selectFirst("div.breadcrumb-trail > ul.trail-items")!!
return SManga.create().apply {
title = trailItemsEl.selectFirst("li.trail-end")!!.text()
genre = trailItemsEl.select("li:not(.trail-end):not(.trail-begin)").joinToString { it.text() }
}
}
override fun chapterListSelector() = "html"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
chapter_number = 0F
setUrlWithoutDomain(element.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href"))
date_upload = POST_DATE_FORMAT.tryParse(element.selectFirst("span.item-metadata.posts-date")?.text())
name = "Gallery"
}
// endregion
// region Pages
override fun pageListParse(document: Document): List<Page> {
val basePageUrl = document.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href")
val maxPage: Int = document.selectFirst("div.nav-links > a.next.page-numbers")?.text()?.toInt() ?: 1
var pageIndex = 0
return (1..maxPage).flatMap { pageNum ->
val doc = if (pageNum == 1) {
document
} else {
client.newCall(GET("$basePageUrl?p=$pageNum", headers)).execute().asJsoup()
}
doc.select("div.entry-content.read-details img.wp-image")
.map { Page(pageIndex++, imageUrl = it.absUrl("src")) }
}
}
// endregion
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Unable to further search in the category!"),
Filter.Separator(),
SourceCategorySelector.create(baseUrl),
)
companion object {
private val POST_DATE_FORMAT = SimpleDateFormat("EEE MMM dd yyyy", Locale.US)
}
}

View File

@ -1,44 +0,0 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
abstract fun simpleMangaSelector(): String
abstract fun simpleMangaFromElement(element: Element): SManga
abstract fun simpleNextPageSelector(): String?
// region popular
override fun popularMangaSelector() = simpleMangaSelector()
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
// endregion
// region last
override fun latestUpdatesSelector() =
if (supportsLatest) simpleMangaSelector() else throw throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) =
if (supportsLatest) simpleMangaFromElement(element) else throw throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() =
if (supportsLatest) simpleNextPageSelector() else throw throw UnsupportedOperationException()
// endregion
// region search
override fun searchMangaSelector() = simpleMangaSelector()
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
// endregion
override fun chapterListSelector() = simpleMangaSelector()
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// endregion
}

View File

@ -1,50 +0,0 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl.Companion.toHttpUrl
data class SourceCategory(private val name: String, var cat: String) {
override fun toString() = this.name
fun buildUrl(baseUrl: String): String {
return "$baseUrl/".toHttpUrl().newBuilder()
.addEncodedQueryParameter("cat", this.cat)
.build()
.toString()
}
}
class SourceCategorySelector(
name: String,
categories: List<SourceCategory>,
) : Filter.Select<SourceCategory>(name, categories.toTypedArray()) {
val selectedCategory: SourceCategory?
get() = if (state > 0) values[state] else null
companion object {
fun create(baseUrl: String): SourceCategorySelector {
val options = listOf(
SourceCategory("unselected", ""),
SourceCategory("大胸美女", "YmpydEtkNzV5NHJKcDJYVGtOVW0yZz09"),
SourceCategory("巨乳美女", "Q09EdlMvMHgweERrUitScTFTaDM4Zz09"),
SourceCategory("全裸写真", "eXZzejJPNFRVNzJqKzFDUmNzZEU2QT09"),
SourceCategory("chinese", "bG9LamJsWWdSbGcyY0FEZytldkhTZz09"),
SourceCategory("chinese models", "OCtTSEI2YzRTcWMvWUsyeDM0aHdzdUIwWDlHMERZUEZaVHUwUEVUVWo3QT0"),
SourceCategory("korean", "Tm1ydGlaZ1A2YWM3a3BvYWh6L3dIdz09"),
SourceCategory("korea", "bzRjeWR0akQrRWpxRE1xOGF6TW5Tdz09"),
SourceCategory("korean models", "TGZTVGtwOCtxTW1TQU1KYWhUb01DQT09"),
SourceCategory("big boobs", "UmFLQVkvVndGNlpPckwvZkpVaEE4UT09"),
SourceCategory("adult", "b2RFSnlwdWxyREMxVmRpcThKVXRLUT09"),
SourceCategory("nude-art", "djFqa293VmFZMEJLdDlUWndsMGtldz09"),
SourceCategory("Asian adult photo", "SHBGZHFueTVNeUlxVHRLaU53RjU2NS9VcjNxRVg3VnhqTGJoK25YaVQ1UT0"),
SourceCategory("cosplay", "OEI2c000ZDBxakwydjZIUVJaRnlMQT09"),
SourceCategory("hot", "c3VRb3RJZ2wrU2tTYmpGSUVqMnFndz09"),
SourceCategory("big breast", "dkQ3b0RiK0xpZDRlMVNSY3lUNkJXQT09"),
)
return SourceCategorySelector("Category", options)
}
}
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Bato.to' extName = 'Bato.to'
extClass = '.BatoToFactory' extClass = '.BatoToFactory'
extVersionCode = 50 extVersionCode = 49
isNsfw = true isNsfw = true
} }

View File

@ -279,7 +279,7 @@ open class BatoTo(
manga.title = infoElement.select("h3").text().removeEntities() manga.title = infoElement.select("h3").text().removeEntities()
manga.thumbnail_url = document.select("div.attr-cover img") manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src") .attr("abs:src")
manga.setUrlWithoutDomain(infoElement.select("h3 a").attr("abs:href")) manga.url = infoElement.select("h3 a").attr("abs:href")
return MangasPage(listOf(manga), false) return MangasPage(listOf(manga), false)
} }
@ -405,7 +405,7 @@ open class BatoTo(
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser()) return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
.select("channel > item").map { item -> .select("channel > item").map { item ->
SChapter.create().apply { SChapter.create().apply {
setUrlWithoutDomain(item.selectFirst("guid")!!.text()) url = item.selectFirst("guid")!!.text()
name = item.selectFirst("title")!!.text() name = item.selectFirst("title")!!.text()
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text()) date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
} }

View File

@ -1,12 +1,8 @@
ext { ext {
extName = 'Buon Dua' extName = 'Buon Dua'
extClass = '.BuonDua' extClass = '.BuonDua'
extVersionCode = 4 extVersionCode = 2
isNsfw = true isNsfw = true
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:randomua"))
}

View File

@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.extension.all.buondua package eu.kanade.tachiyomi.extension.all.buondua
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -11,15 +8,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
class BuonDua() : ParsedHttpSource() { class BuonDua() : ParsedHttpSource() {
override val baseUrl = "https://buondua.com" override val baseUrl = "https://buondua.com"
@ -27,13 +20,6 @@ class BuonDua() : ParsedHttpSource() {
override val name = "Buon Dua" override val name = "Buon Dua"
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
.setRandomUserAgent(UserAgentType.MOBILE)
.build()
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
// Latest // Latest
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
@ -57,10 +43,10 @@ class BuonDua() : ParsedHttpSource() {
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/hot?start=${20 * (page - 1)}") return GET("$baseUrl/hot?start=${20 * (page - 1)}")
} }
override fun popularMangaSelector() = latestUpdatesSelector() override fun popularMangaSelector() = latestUpdatesSelector()
// Search // Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -71,7 +57,6 @@ class BuonDua() : ParsedHttpSource() {
else -> popularMangaRequest(page) else -> popularMangaRequest(page)
} }
} }
override fun searchMangaSelector() = latestUpdatesSelector() override fun searchMangaSelector() = latestUpdatesSelector()
// Details // Details
@ -87,27 +72,34 @@ class BuonDua() : ParsedHttpSource() {
return manga return manga
} }
override fun chapterListSelector() = throw UnsupportedOperationException() override fun chapterFromElement(element: Element): SChapter {
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() val chapter = SChapter.create()
override fun chapterListParse(response: Response): List<SChapter> { chapter.setUrlWithoutDomain(element.select(".is-current").first()!!.attr("abs:href"))
val doc = response.asJsoup() chapter.chapter_number = 0F
val dateUploadStr = doc.selectFirst(".article-info > small")?.text() chapter.name = element.select(".article-header").text()
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr) chapter.date_upload = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US).parse(element.select(".article-info > small").text())?.time ?: 0L
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1 return chapter
val basePageUrl = response.request.url
return (maxPage downTo 1).map { page ->
SChapter.create().apply {
setUrlWithoutDomain("$basePageUrl?page=$page")
name = "Page $page"
date_upload = dateUpload
}
}
} }
override fun chapterListSelector() = "html"
// Pages // Pages
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select(".article-fulltext img") val numpages = document.selectFirst(".pagination-list")!!.select(".pagination-link")
.mapIndexed { i, imgEl -> Page(i, imageUrl = imgEl.absUrl("src")) } val pages = mutableListOf<Page>()
numpages.forEachIndexed { index, page ->
val doc = when (index) {
0 -> document
else -> client.newCall(GET(page.attr("abs:href"))).execute().asJsoup()
}
doc.select(".article-fulltext img").forEach {
val itUrl = it.attr("abs:src")
pages.add(Page(pages.size, "", itUrl))
}
}
return pages
} }
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -122,8 +114,4 @@ class BuonDua() : ParsedHttpSource() {
class TagFilter : Filter.Text("Tag ID") class TagFilter : Filter.Text("Tag ID")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
companion object {
private val DATE_FORMAT = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US)
}
} }

View File

@ -1,7 +1,5 @@
ignored_groups_title=Ignored Groups ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive) ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
ignored_tags_title=Ignored Tags
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description show_alternative_titles_off=Does not show alternative titles to the description

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comick' extName = 'Comick'
extClass = '.ComickFactory' extClass = '.ComickFactory'
extVersionCode = 56 extVersionCode = 55
isNsfw = true isNsfw = true
} }

View File

@ -79,12 +79,6 @@ abstract class Comick(
} }
}.also(screen::addPreference) }.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = IGNORED_TAGS_PREF
title = intl["ignored_tags_title"]
summary = intl["ignored_tags_summary"]
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"] title = intl["show_alternative_titles_title"]
@ -190,14 +184,6 @@ abstract class Comick(
.orEmpty() .orEmpty()
.toSet() .toSet()
private val SharedPreferences.ignoredTags: String
get() = getString(IGNORED_TAGS_PREF, "")
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
.orEmpty()
.joinToString(",")
private val SharedPreferences.showAlternativeTitles: Boolean private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT) get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
@ -257,13 +243,8 @@ abstract class Comick(
/** Popular Manga **/ /** Popular Manga **/
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest( val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
page = page, return GET(url, headers)
query = "",
filters = FilterList(
SortFilter("follow"),
),
)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
@ -276,13 +257,8 @@ abstract class Comick(
/** Latest Manga **/ /** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest( val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$LIMIT&page=$page&tachiyomi=true"
page = page, return GET(url, headers)
query = "",
filters = FilterList(
SortFilter("uploaded"),
),
)
} }
override fun latestUpdatesParse(response: Response) = popularMangaParse(response) override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
@ -340,7 +316,7 @@ abstract class Comick(
} }
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) { private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").filter(String::isNotEmpty).forEach { tags.split(",").forEach {
builder.addQueryParameter( builder.addQueryParameter(
parameterName, parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-") it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
@ -436,7 +412,6 @@ abstract class Comick(
else -> {} else -> {}
} }
} }
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
addQueryParameter("tachiyomi", "true") addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$LIMIT") addQueryParameter("limit", "$LIMIT")
addQueryParameter("page", "$page") addQueryParameter("page", "$page")
@ -612,7 +587,6 @@ abstract class Comick(
const val SLUG_SEARCH_PREFIX = "id:" const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]") private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups" private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val IGNORED_TAGS_PREF = "IgnoredTags"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles" private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags" private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"

View File

@ -9,7 +9,7 @@ fun getFilters(): FilterList {
GenreFilter("Genre", getGenresList), GenreFilter("Genre", getGenresList),
DemographicFilter("Demographic", getDemographicList), DemographicFilter("Demographic", getDemographicList),
TypeFilter("Type", getTypeList), TypeFilter("Type", getTypeList),
SortFilter(), SortFilter("Sort", getSortsList),
StatusFilter("Status", getStatusList), StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList), ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"), CompletedFilter("Completely Scanlated?"),
@ -50,8 +50,8 @@ internal class FromYearFilter(name: String) : TextFilter(name)
internal class ToYearFilter(name: String) : TextFilter(name) internal class ToYearFilter(name: String) : TextFilter(name)
internal class SortFilter(defaultValue: String? = null, state: Int = 0) : internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter("Sort", getSortsList, state, defaultValue) SelectFilter(name, sortList, state)
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) : internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state) SelectFilter(name, statusList, state)
@ -66,8 +66,8 @@ internal open class TextFilter(name: String) : Filter.Text(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name) internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0, defaultValue: String? = null) : internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) { Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second fun getValue() = vals[state].second
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'DeviantArt' extName = 'DeviantArt'
extClass = '.DeviantArt' extClass = '.DeviantArt'
extVersionCode = 8 extVersionCode = 7
isNsfw = true isNsfw = true
} }

View File

@ -134,11 +134,12 @@ class DeviantArt : HttpSource(), ConfigurableSource {
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href") nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
} }
return chapterList.also(::orderChapterList).toList() return chapterList.toList().also(::indexChapterList)
} }
private fun parseToChapterList(document: Document): List<SChapter> { private fun parseToChapterList(document: Document): List<SChapter> {
return document.select("item").map { val items = document.select("item")
return items.map {
SChapter.create().apply { SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text()) setUrlWithoutDomain(it.selectFirst("link")!!.text())
name = it.selectFirst("title")!!.text() name = it.selectFirst("title")!!.text()
@ -148,15 +149,17 @@ class DeviantArt : HttpSource(), ConfigurableSource {
} }
} }
private fun orderChapterList(chapterList: MutableList<SChapter>) { private fun indexChapterList(chapterList: List<SChapter>) {
// In Mihon's updates tab, chapters are ordered by source instead // DeviantArt allows users to arrange galleries arbitrarily so we will
// of chapter number, so to avoid updates being shown in reverse, // primitively index the list by checking the first and last dates
// disregard source order and order chronologically instead if (chapterList.first().date_upload > chapterList.last().date_upload) {
if (chapterList.first().date_upload < chapterList.last().date_upload) { chapterList.forEachIndexed { i, chapter ->
chapterList.reverse() chapter.chapter_number = chapterList.size - i.toFloat()
} }
chapterList.forEachIndexed { i, chapter -> } else {
chapter.chapter_number = chapterList.size - i.toFloat() chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = i.toFloat() + 1
}
} }
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.EternalMangasFactory' extClass = '.EternalMangasFactory'
themePkg = 'mangaesp' themePkg = 'mangaesp'
baseUrl = 'https://eternalmangas.com' baseUrl = 'https://eternalmangas.com'
overrideVersionCode = 5 overrideVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -2,24 +2,16 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
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.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rx.Observable
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -34,58 +26,16 @@ open class EternalMangas(
) { ) {
override val useApiSearch = true override val useApiSearch = true
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun latestUpdatesParse(response: Response): MangasPage {
return super.fetchSearchManga(page, "", createSortFilter("views", false)) val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
} val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga(seriesPath) } ?: emptyList()
return MangasPage(mangas, false)
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("updated_at", false))
} }
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> { override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
return this.filter { it.language == internalLang }.toMutableList() return this.filter { it.language == internalLang }.toMutableList()
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/comics", headers)
}
private val dataUrl = "https://raw.githubusercontent.com/bapeey/extensions-tools/refs/heads/main/keiyoushi/eternalmangas/values.txt"
override fun searchMangaParse(
response: Response,
page: Int,
query: String,
filters: FilterList,
): MangasPage {
val (apiComicsUrl, jsonHeaders, useApi, scriptSelector, comicsRegex) = client.newCall(GET(dataUrl)).execute().body.string().split("\n")
val apiSearch = useApi == "1"
comicsList = if (apiSearch) {
val headersJson = json.parseToJsonElement(jsonHeaders).jsonObject
val apiHeaders = headersBuilder()
headersJson.forEach { (key, jsonElement) ->
var value = jsonElement.jsonPrimitive.contentOrNull.orEmpty()
if (value.startsWith("1-")) {
val match = value.substringAfter("-").toRegex().find(response.body.string())
value = match?.groupValues?.get(1).orEmpty()
} else {
value = value.substringAfter("-")
}
apiHeaders.add(key, value)
}
val apiResponse = client.newCall(GET(apiComicsUrl, apiHeaders.build())).execute()
json.decodeFromString<List<SeriesDto>>(apiResponse.body.string()).toMutableList()
} else {
val script = response.asJsoup().select(scriptSelector).joinToString { it.data() }
val jsonString = comicsRegex.toRegex().find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
}
return parseComicsList(page, query, filters)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply { override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = jsRedirect(response) val body = jsRedirect(response)
@ -142,7 +92,7 @@ open class EternalMangas(
private fun jsRedirect(response: Response): String { private fun jsRedirect(response: Response): String {
var body = response.body.string() var body = response.body.string()
val document = Jsoup.parse(body) val document = Jsoup.parse(body)
document.selectFirst("body > form[method=post], body > div[hidden] > form[method=post]")?.let { document.selectFirst("body > form[method=post]")?.let {
val action = it.attr("action") val action = it.attr("action")
val inputs = it.select("input") val inputs = it.select("input")
@ -156,13 +106,8 @@ open class EternalMangas(
return body return body
} }
private fun createSortFilter(value: String, ascending: Boolean = false): FilterList { @Serializable
val sortProperties = getSortProperties() class LatestUpdatesDto(
val index = sortProperties.indexOfFirst { it.value == value }.takeIf { it >= 0 } ?: 0 val updates: Map<String, List<List<SeriesDto>>>,
return FilterList( )
SortByFilter("", sortProperties).apply {
state = Filter.Sort.Selection(index, ascending)
},
)
}
} }

View File

@ -1,5 +0,0 @@
data_saver=Data saver
data_saver_summary=Enables smaller, more compressed images
invalid_manga_id=Not a valid comic ID
show_locked_chapters=Show chapters with pay-walled pages
show_locked_chapters_summary=Display chapters that require an account with a premium subscription

View File

@ -1,12 +0,0 @@
ext {
extName = 'GlobalComix'
extClass = '.GlobalComixFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:i18n"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChapterDataDto.Companion.createChapter
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChapterDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChaptersDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.EntityDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangaDataDto.Companion.createManga
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangaDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangasDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.UnknownEntity
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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 keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
abstract class GlobalComix(final override val lang: String, private val extLang: String = lang) :
ConfigurableSource, HttpSource() {
override val name = "GlobalComix"
override val baseUrl = webUrl
override val supportsLatest = true
private val preferences: SharedPreferences by getPreferencesLazy()
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
serializersModule += SerializersModule {
polymorphic(EntityDto::class) {
defaultDeserializer { UnknownEntity.serializer() }
}
}
}
private val intl = Intl(
language = lang,
baseLanguage = english,
availableLanguages = setOf(english),
classLoader = this::class.java.classLoader!!,
createMessageFileName = { lang -> Intl.createDefaultMessageFileName(lang) },
)
final override fun headersBuilder() = super.headersBuilder().apply {
set("Referer", "$baseUrl/")
set("Origin", baseUrl)
set("x-gc-client", clientId)
set("x-gc-identmode", "cookie")
}
override val client = network.client.newBuilder()
.rateLimit(3)
.build()
private fun simpleQueryRequest(page: Int, orderBy: String?, query: String?): Request {
val url = apiSearchUrl.toHttpUrl().newBuilder()
.addQueryParameter("lang_id[]", extLang)
.addQueryParameter("p", page.toString())
orderBy?.let { url.addQueryParameter("sort", it) }
query?.let { url.addQueryParameter("q", it) }
return GET(url.build(), headers)
}
override fun popularMangaRequest(page: Int): Request =
simpleQueryRequest(page, orderBy = null, query = null)
override fun popularMangaParse(response: Response): MangasPage =
mangaListParse(response)
override fun latestUpdatesRequest(page: Int): Request =
simpleQueryRequest(page, "recent", query = null)
override fun latestUpdatesParse(response: Response): MangasPage =
mangaListParse(response)
private fun mangaListParse(response: Response): MangasPage {
val isSingleItemLookup = response.request.url.toString().startsWith(apiMangaUrl)
return if (!isSingleItemLookup) {
// Normally, the response is a paginated list of mangas
// The results property will be a JSON array
response.parseAs<MangasDto>().payload!!.let { dto ->
MangasPage(
dto.results.map { it -> it.createManga() },
dto.pagination.hasNextPage,
)
}
} else {
// However, when using the 'id:' query prefix (via the UrlActivity for example),
// the response is a single manga and the results property will be a JSON object
MangasPage(
listOf(
response.parseAs<MangaDto>().payload!!
.results
.createManga(),
),
false,
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// If the query is a slug ID, return the manga directly
if (query.startsWith(prefixIdSearch)) {
val mangaSlugId = query.removePrefix(prefixIdSearch)
if (mangaSlugId.isEmpty()) {
throw Exception(intl["invalid_manga_id"])
}
val url = apiMangaUrl.toHttpUrl().newBuilder()
.addPathSegment(mangaSlugId)
.build()
return GET(url, headers)
}
return simpleQueryRequest(page, orderBy = "relevance", query)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun getMangaUrl(manga: SManga): String = "$webComicUrl/${titleToSlug(manga.title)}"
override fun mangaDetailsRequest(manga: SManga): Request {
val url = apiMangaUrl.toHttpUrl().newBuilder()
.addPathSegment(titleToSlug(manga.title))
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<MangaDto>().payload!!
.results
.createManga()
override fun chapterListRequest(manga: SManga): Request {
val url = apiSearchUrl.toHttpUrl().newBuilder()
.addPathSegment(manga.url) // manga.url contains the the comic id
.addPathSegment("releases")
.addQueryParameter("lang_id", extLang)
.addQueryParameter("all", "true")
.toString()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> =
response.parseAs<ChaptersDto>().payload!!.results.filterNot { dto ->
dto.isPremium && !preferences.showLockedChapters
}.map { it.createChapter() }
override fun getChapterUrl(chapter: SChapter): String =
"$baseUrl/read/${chapter.url}"
override fun pageListRequest(chapter: SChapter): Request {
val chapterKey = chapter.url
val url = "$apiChapterUrl/$chapterKey"
return GET(url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val chapterKey = response.request.url.pathSegments.last()
val chapterWebUrl = "$webChapterUrl/$chapterKey"
return response.parseAs<ChapterDto>()
.payload!!
.results
.page_objects!!
.map { dto -> if (preferences.useDataSaver) dto.mobile_image_url else dto.desktop_image_url }
.mapIndexed { index, url -> Page(index, "$chapterWebUrl/$index", url) }
}
override fun imageUrlParse(response: Response): String = ""
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
key = getDataSaverPreferenceKey(extLang)
title = intl["data_saver"]
summary = intl["data_saver_summary"]
setDefaultValue(false)
}
val showLockedChaptersPref = SwitchPreferenceCompat(screen.context).apply {
key = getShowLockedChaptersPreferenceKey(extLang)
title = intl["show_locked_chapters"]
summary = intl["show_locked_chapters_summary"]
setDefaultValue(true)
}
screen.addPreference(dataSaverPref)
screen.addPreference(showLockedChaptersPref)
}
private inline fun <reified T> Response.parseAs(): T = parseAs(json)
private val SharedPreferences.useDataSaver
get() = getBoolean(getDataSaverPreferenceKey(extLang), false)
private val SharedPreferences.showLockedChapters
get() = getBoolean(getShowLockedChaptersPreferenceKey(extLang), true)
companion object {
fun titleToSlug(title: String) = title.trim()
.lowercase(Locale.US)
.replace(titleSpecialCharactersRegex, "-")
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
}
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
const val lockSymbol = "🔒"
// Language codes used for translations
const val english = "en"
// JSON discriminators
const val release = "Release"
const val comic = "Comic"
const val artist = "Artist"
const val releasePage = "ReleasePage"
// Web requests
const val webUrl = "https://globalcomix.com"
const val webComicUrl = "$webUrl/c"
const val webChapterUrl = "$webUrl/read"
const val apiUrl = "https://api.globalcomix.com/v1"
const val apiMangaUrl = "$apiUrl/read"
const val apiChapterUrl = "$apiUrl/readV2"
const val apiSearchUrl = "$apiUrl/comics"
const val clientId = "gck_d0f170d5729446dcb3b55e6b3ebc7bf6"
// Search prefix for title ids
const val prefixIdSearch = "id:"
// Preferences
fun getDataSaverPreferenceKey(extLang: String): String = "dataSaver_$extLang"
fun getShowLockedChaptersPreferenceKey(extLang: String): String = "showLockedChapters_$extLang"

View File

@ -1,92 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class GlobalComixFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
GlobalComixAlbanian(),
GlobalComixArabic(),
GlobalComixBulgarian(),
GlobalComixBengali(),
GlobalComixBrazilianPortuguese(),
GlobalComixChineseMandarin(),
GlobalComixCzech(),
GlobalComixGerman(),
GlobalComixDanish(),
GlobalComixGreek(),
GlobalComixEnglish(),
GlobalComixSpanish(),
GlobalComixPersian(),
GlobalComixFinnish(),
GlobalComixFilipino(),
GlobalComixFrench(),
GlobalComixHindi(),
GlobalComixHungarian(),
GlobalComixIndonesian(),
GlobalComixItalian(),
GlobalComixHebrew(),
GlobalComixJapanese(),
GlobalComixKorean(),
GlobalComixLatvian(),
GlobalComixMalay(),
GlobalComixDutch(),
GlobalComixNorwegian(),
GlobalComixPolish(),
GlobalComixPortugese(),
GlobalComixRomanian(),
GlobalComixRussian(),
GlobalComixSwedish(),
GlobalComixSlovak(),
GlobalComixSlovenian(),
GlobalComixTamil(),
GlobalComixThai(),
GlobalComixTurkish(),
GlobalComixUkrainian(),
GlobalComixUrdu(),
GlobalComixVietnamese(),
GlobalComixChineseCantonese(),
)
}
class GlobalComixAlbanian : GlobalComix("al")
class GlobalComixArabic : GlobalComix("ar")
class GlobalComixBulgarian : GlobalComix("bg")
class GlobalComixBengali : GlobalComix("bn")
class GlobalComixBrazilianPortuguese : GlobalComix("pt-BR", "br")
class GlobalComixChineseMandarin : GlobalComix("zh-Hans", "cn")
class GlobalComixCzech : GlobalComix("cs", "cz")
class GlobalComixGerman : GlobalComix("de")
class GlobalComixDanish : GlobalComix("dk")
class GlobalComixGreek : GlobalComix("el")
class GlobalComixEnglish : GlobalComix("en")
class GlobalComixSpanish : GlobalComix("es")
class GlobalComixPersian : GlobalComix("fa")
class GlobalComixFinnish : GlobalComix("fi")
class GlobalComixFilipino : GlobalComix("fil", "fo")
class GlobalComixFrench : GlobalComix("fr")
class GlobalComixHindi : GlobalComix("hi")
class GlobalComixHungarian : GlobalComix("hu")
class GlobalComixIndonesian : GlobalComix("id")
class GlobalComixItalian : GlobalComix("it")
class GlobalComixHebrew : GlobalComix("he", "iw")
class GlobalComixJapanese : GlobalComix("ja", "jp")
class GlobalComixKorean : GlobalComix("ko", "kr")
class GlobalComixLatvian : GlobalComix("lv")
class GlobalComixMalay : GlobalComix("ms", "my")
class GlobalComixDutch : GlobalComix("nl")
class GlobalComixNorwegian : GlobalComix("no")
class GlobalComixPolish : GlobalComix("pl")
class GlobalComixPortugese : GlobalComix("pt")
class GlobalComixRomanian : GlobalComix("ro")
class GlobalComixRussian : GlobalComix("ru")
class GlobalComixSwedish : GlobalComix("sv", "se")
class GlobalComixSlovak : GlobalComix("sk")
class GlobalComixSlovenian : GlobalComix("sl")
class GlobalComixTamil : GlobalComix("ta")
class GlobalComixThai : GlobalComix("th")
class GlobalComixTurkish : GlobalComix("tr")
class GlobalComixUkrainian : GlobalComix("uk", "ua")
class GlobalComixUrdu : GlobalComix("ur")
class GlobalComixVietnamese : GlobalComix("vi")
class GlobalComixChineseCantonese : GlobalComix("zh-Hant", "zh")

View File

@ -1,45 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import kotlin.system.exitProcess
/**
* Springboard that accepts https://globalcomix.com/c/xxx intents and redirects them to
* the main tachiyomi process. The idea is to not install the intent filter unless
* you have this extension installed, but still let the main tachiyomi app control
* things.
*/
class GlobalComixUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
// Supported path: /c/title-slug
if (pathSegments != null && pathSegments.size > 1) {
val titleId = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", prefixIdSearch + titleId)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("GlobalComixUrlActivity", e.toString())
}
} else {
Log.e("GlobalComixUrlActivity", "Received data URL is unsupported: ${intent?.data}")
Toast.makeText(this, "This URL cannot be handled by the GlobalComix extension.", Toast.LENGTH_SHORT).show()
}
finish()
exitProcess(0)
}
}

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.artist
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
@SerialName(artist)
class ArtistDto(
val name: String, // Slug
val roman_name: String?,
) : EntityDto()

View File

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.GlobalComix.Companion.dateFormatter
import eu.kanade.tachiyomi.extension.all.globalcomix.lockSymbol
import eu.kanade.tachiyomi.extension.all.globalcomix.release
import eu.kanade.tachiyomi.source.model.SChapter
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias ChapterDto = ResponseDto<ChapterDataDto>
typealias ChaptersDto = PaginatedResponseDto<ChapterDataDto>
@Suppress("PropertyName")
@Serializable
@SerialName(release)
class ChapterDataDto(
val title: String,
val chapter: String, // Stringified number
val key: String, // UUID, required for /readV2 endpoint
val premium_only: Int? = 0,
val published_time: String,
// Only available when calling the /readV2 endpoint
val page_objects: List<PageDataDto>?,
) : EntityDto() {
val isPremium: Boolean
get() = premium_only == 1
companion object {
/**
* Create an [SChapter] instance from the JSON DTO element.
*/
fun ChapterDataDto.createChapter(): SChapter {
val chapterName = mutableListOf<String>()
if (isPremium) {
chapterName.add(lockSymbol)
}
chapter.let {
if (it.isNotEmpty()) {
chapterName.add("Ch.$it")
}
}
title.let {
if (it.isNotEmpty()) {
if (chapterName.isNotEmpty()) {
chapterName.add("-")
}
chapterName.add(it)
}
}
return SChapter.create().apply {
url = key
name = chapterName.joinToString(" ")
chapter_number = chapter.toFloatOrNull() ?: 0f
date_upload = dateFormatter.tryParse(published_time)
}
}
}
}

View File

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import kotlinx.serialization.Serializable
@Serializable
sealed class EntityDto {
val id: Long = -1
}
@Serializable
class UnknownEntity() : EntityDto()

View File

@ -1,49 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.comic
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias MangaDto = ResponseDto<MangaDataDto>
typealias MangasDto = PaginatedResponseDto<MangaDataDto>
@Suppress("PropertyName")
@Serializable
@SerialName(comic)
class MangaDataDto(
val name: String,
val description: String?,
val status_name: String?,
val category_name: String?,
val image_url: String?,
val artist: ArtistDto,
) : EntityDto() {
companion object {
/**
* Create an [SManga] instance from the JSON DTO element.
*/
fun MangaDataDto.createManga(): SManga =
SManga.create().also {
it.initialized = true
it.url = id.toString()
it.description = description
it.author = artist.let { it.roman_name ?: it.name }
it.status = status_name?.let(::convertStatus) ?: SManga.UNKNOWN
it.genre = category_name
it.title = name
it.thumbnail_url = image_url
}
private fun convertStatus(status: String): Int {
return when (status) {
"Ongoing" -> SManga.ONGOING
"Preview" -> SManga.ONGOING
"Finished" -> SManga.COMPLETED
"On hold" -> SManga.ON_HIATUS
"Cancelled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
}
}

View File

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.releasePage
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
@SerialName(releasePage)
class PageDataDto(
val is_page_paid: Boolean,
val desktop_image_url: String,
val mobile_image_url: String,
) : EntityDto()

View File

@ -1,36 +0,0 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import kotlinx.serialization.Serializable
@Serializable
class PaginatedResponseDto<T : EntityDto>(
val payload: PaginatedPayloadDto<T>? = null,
)
@Serializable
class PaginatedPayloadDto<T : EntityDto>(
val results: List<T> = emptyList(),
val pagination: PaginationStateDto,
)
@Serializable
class ResponseDto<T : EntityDto>(
val payload: PayloadDto<T>? = null,
)
@Serializable
class PayloadDto<T : EntityDto>(
val results: T,
)
@Suppress("PropertyName")
@Serializable
class PaginationStateDto(
val page: Int = 1,
val per_page: Int = 0,
val total_pages: Int = 0,
val total_results: Int = 0,
) {
val hasNextPage: Boolean
get() = page < total_pages
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hitomi' extName = 'Hitomi'
extClass = '.HitomiFactory' extClass = '.HitomiFactory'
extVersionCode = 39 extVersionCode = 38
isNsfw = true isNsfw = true
} }

View File

@ -20,7 +20,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -52,7 +52,7 @@ class Hitomi(
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageUrlInterceptor) .addInterceptor(::updateImageUrlInterceptor)
.apply { .apply {
interceptors().add(0, ::streamResetRetry) interceptors().add(0, ::streamResetRetry)
} }
@ -491,20 +491,18 @@ class Hitomi(
}.awaitAll().filterNotNull() }.awaitAll().filterNotNull()
} }
private fun Gallery.toSManga() = SManga.create().apply { private suspend fun Gallery.toSManga() = SManga.create().apply {
title = this@toSManga.title title = this@toSManga.title
url = galleryurl url = galleryurl
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted } author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted } artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.formatted } genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let { thumbnail_url = files.first().let {
HttpUrl.Builder().apply { val hash = it.hash
scheme("https") val imageId = imageIdFromHash(hash)
host(IMAGE_LOOPBACK_HOST) val subDomain = 'a' + subdomainOffset(imageId)
addQueryParameter(IMAGE_THUMBNAIL, "true")
addQueryParameter(IMAGE_GIF, it.isGif.toString()) "https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
fragment(it.hash)
}.toString()
} }
description = buildString { description = buildString {
japaneseTitle?.let { japaneseTitle?.let {
@ -573,13 +571,11 @@ class Hitomi(
.substringBefore(".") .substringBefore(".")
return gallery.files.mapIndexed { idx, img -> return gallery.files.mapIndexed { idx, img ->
// actual logic in imageUrlInterceptor // actual logic in updateImageUrlInterceptor
val imageUrl = HttpUrl.Builder().apply { val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
scheme("https") .fragment(img.hash)
host(IMAGE_LOOPBACK_HOST) .build()
addQueryParameter(IMAGE_GIF, img.isGif.toString()) .toString()
fragment(img.hash)
}.toString()
Page( Page(
idx, idx,
@ -681,38 +677,18 @@ class Hitomi(
} }
} }
private fun imageUrlInterceptor(chain: Interceptor.Chain): Response { private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
if (request.url.host != IMAGE_LOOPBACK_HOST) { if (request.url.host != "127.0.0.1") {
return chain.proceed(request) return chain.proceed(request)
} }
val hash = request.url.fragment!! val hash = request.url.fragment!!
val isThumbnail = request.url.queryParameter(IMAGE_THUMBNAIL) == "true" val commonId = runBlocking { commonImageId() }
val isGif = request.url.queryParameter(IMAGE_GIF) == "true"
val type = if (isGif) {
"webp"
} else {
"avif"
}
val imageId = imageIdFromHash(hash) val imageId = imageIdFromHash(hash)
val subDomainOffset = runBlocking { subdomainOffset(imageId) } val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
val imageUrl = if (isThumbnail) { val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
val subDomain = "${'a' + subDomainOffset}tn"
"https://$subDomain.$cdnDomain/${type}bigtn/${thumbPathFromHash(hash)}/$hash.$type"
} else {
val commonId = runBlocking { commonImageId() }
val subDomain = if (isGif) {
"w${subDomainOffset + 1}"
} else {
"a${subDomainOffset + 1}"
}
"https://$subDomain.$cdnDomain/$commonId$imageId/$hash.$type"
}
val newRequest = request.newBuilder() val newRequest = request.newBuilder()
.url(imageUrl) .url(imageUrl)
@ -729,7 +705,3 @@ class Hitomi(
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
} }
const val IMAGE_LOOPBACK_HOST = "127.0.0.1"
const val IMAGE_THUMBNAIL = "is_thumbnail"
const val IMAGE_GIF = "is_gif"

View File

@ -22,10 +22,7 @@ class Gallery(
@Serializable @Serializable
class ImageFile( class ImageFile(
val hash: String, val hash: String,
private val name: String, )
) {
val isGif get() = name.endsWith(".gif")
}
@Serializable @Serializable
class Tag( class Tag(

View File

@ -3,7 +3,7 @@ ext {
extClass = '.KdtScans' extClass = '.KdtScans'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://kdtscans.com' baseUrl = 'https://kdtscans.com'
overrideVersionCode = 2 overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

View File

@ -4,11 +4,14 @@ import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class KdtScans : Madara( class KdtScans : Madara(
"KDT Scans", "KDT Scans",
"https://kdtscans.com", "https://kdtscans.com",
"all", "all",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
) { ) {
override val useNewChapterEndpoint = true override val useNewChapterEndpoint = true
override val fetchGenres = false override val fetchGenres = false
@ -19,8 +22,6 @@ class KdtScans : Madara(
} }
} }
override fun searchMangaSelector() = "div.c-tabs-item__content:not(:contains([LN]))"
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga {
return super.searchMangaFromElement(element).apply { return super.searchMangaFromElement(element).apply {
title = title.cleanupTitle() title = title.cleanupTitle()
@ -36,7 +37,5 @@ class KdtScans : Madara(
private fun String.cleanupTitle() = replace(titleCleanupRegex, "").trim() private fun String.cleanupTitle() = replace(titleCleanupRegex, "").trim()
private val titleCleanupRegex = private val titleCleanupRegex =
Regex("""^\[(ESPAÑOL|English|HD|VIP)\]\s+(\s+)?""", RegexOption.IGNORE_CASE) Regex("""^\[(ESPAÑOL|English)\]\s+(\s+)?""", RegexOption.IGNORE_CASE)
override fun chapterListSelector() = "li.wp-manga-chapter:not(:has(.required-login))"
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Kiutaku' extName = 'Kiutaku'
extClass = '.Kiutaku' extClass = '.Kiutaku'
extVersionCode = 3 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -78,12 +78,7 @@ class Kiutaku : ParsedHttpSource() {
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder() return GET("$baseUrl/?search=$query&start=${getPage(page)}", headers)
.addQueryParameter("search", query)
.addQueryParameter("start", getPage(page).toString())
.build()
return GET(url, headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'SchaleNetwork' extName = 'SchaleNetwork'
extClass = '.KoharuFactory' extClass = '.KoharuFactory'
extVersionCode = 14 extVersionCode = 13
isNsfw = true isNsfw = true
} }

View File

@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.authorization
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -122,9 +120,7 @@ class TurnstileInterceptor(
try { try {
val noRedirectClient = client.newBuilder().followRedirects(false).build() val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders(authHeader) val authHeaders = authHeaders(authHeader)
val response = runBlocking(Dispatchers.IO) { val response = noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
}
response.use { response.use {
if (response.isSuccessful) { if (response.isSuccessful) {
with(response) { with(response) {
@ -180,9 +176,7 @@ class TurnstileInterceptor(
try { try {
val noRedirectClient = client.newBuilder().followRedirects(false).build() val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders("Bearer $token") val authHeaders = authHeaders("Bearer $token")
val response = runBlocking(Dispatchers.IO) { val response = noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
}
response.use { response.use {
if (response.isSuccessful) { if (response.isSuccessful) {
return true return true

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Komga' extName = 'Komga'
extClass = '.KomgaFactory' extClass = '.KomgaFactory'
extVersionCode = 63 extVersionCode = 61
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -134,12 +134,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
else -> "series" else -> "series"
} }
val url = "$baseUrl/api/v1".toHttpUrl().newBuilder() val url = "$baseUrl/api/v1/$type?search=$query&page=${page - 1}&deleted=false".toHttpUrl().newBuilder()
.addPathSegments(type)
.addQueryParameter("search", query)
.addQueryParameter("page", (page - 1).toString())
.addQueryParameter("deleted", "false")
val filterList = filters.ifEmpty { getFilterList() } val filterList = filters.ifEmpty { getFilterList() }
val defaultLibraries = defaultLibraries val defaultLibraries = defaultLibraries
@ -188,7 +183,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "") override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers) override fun mangaDetailsRequest(manga: SManga) = GET(manga.url)
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
return if (response.isFromReadList()) { return if (response.isFromReadList()) {
@ -259,7 +254,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
.sortedByDescending { it.chapter_number } .sortedByDescending { it.chapter_number }
} }
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages", headers) override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages")
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val pages = response.parseAs<List<PageDto>>() val pages = response.parseAs<List<PageDto>>()
@ -472,17 +467,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
scope.launch { scope.launch {
try { try {
libraries = client.newCall(GET("$baseUrl/api/v1/libraries", headers)).await().parseAs() libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).await().parseAs()
collections = client collections = client
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true", headers)) .newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
.await() .await()
.parseAs<PageWrapperDto<CollectionDto>>() .parseAs<PageWrapperDto<CollectionDto>>()
.content .content
genres = client.newCall(GET("$baseUrl/api/v1/genres", headers)).await().parseAs() genres = client.newCall(GET("$baseUrl/api/v1/genres")).await().parseAs()
tags = client.newCall(GET("$baseUrl/api/v1/tags", headers)).await().parseAs() tags = client.newCall(GET("$baseUrl/api/v1/tags")).await().parseAs()
publishers = client.newCall(GET("$baseUrl/api/v1/publishers", headers)).await().parseAs() publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).await().parseAs()
authors = client authors = client
.newCall(GET("$baseUrl/api/v1/authors", headers)) .newCall(GET("$baseUrl/api/v1/authors"))
.await() .await()
.parseAs<List<AuthorDto>>() .parseAs<List<AuthorDto>>()
.groupBy { it.role } .groupBy { it.role }

View File

@ -38,7 +38,7 @@ class SeriesDto(
metadata.status == "HIATUS" -> SManga.ON_HIATUS metadata.status == "HIATUS" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
genre = (metadata.genres + metadata.tags + booksMetadata.tags).sorted().distinct().joinToString(", ") genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
description = metadata.summary.ifBlank { booksMetadata.summary } description = metadata.summary.ifBlank { booksMetadata.summary }
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map -> booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
author = map["writer"]?.distinct()?.joinToString() author = map["writer"]?.distinct()?.joinToString()

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MangaFire' extName = 'MangaFire'
extClass = '.MangaFireFactory' extClass = '.MangaFireFactory'
extVersionCode = 12 extVersionCode = 11
isNsfw = true isNsfw = true
} }

View File

@ -45,9 +45,6 @@ class MangaFire(
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build() override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {

View File

@ -13,7 +13,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" <data android:scheme="https"
android:host="mangago.fit" android:host="mangahosted.org"
android:pathPattern="/.*/..*" /> android:pathPattern="/.*/..*" />
</intent-filter> </intent-filter>

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Manga Hosted' extName = 'Manga Hosted'
extClass = '.MangaHostedFactory' extClass = '.MangaHostedFactory'
extVersionCode = 3 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -26,7 +26,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val name: String = "Manga Hosted${langOption.nameSuffix}" override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangago.fit/${langOption.infix}" override val baseUrl: String = "https://mangahosted.org"
override val supportsLatest = true override val supportsLatest = true
@ -80,7 +80,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) { if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${query.substringAfter(SEARCH_PREFIX)}" val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers)) return client.newCall(GET(url, headers))
.asObservableSuccess().map { response -> .asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() } val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
@ -184,7 +184,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
title = dto.title title = dto.title
thumbnail_url = dto.thumbnailUrl thumbnail_url = dto.thumbnailUrl
status = dto.status status = dto.status
url = "/${dto.slug}" url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres genre = dto.genres
initialized = true initialized = true
} }
@ -195,7 +195,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
companion object { companion object {
const val SEARCH_PREFIX = "slug:" const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.mangago.fit" val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api" val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
} }

View File

@ -23,7 +23,7 @@ val languages = listOf(
LanguageOption("id", "manga-indo", "id"), LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"), LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"), LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br"), LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("ru", "manga-ru", "mangaru"), LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"), LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"), LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MangaPark' extName = 'MangaPark'
extClass = '.MangaParkFactory' extClass = '.MangaParkFactory'
extVersionCode = 22 extVersionCode = 21
isNsfw = true isNsfw = true
} }

View File

@ -17,19 +17,20 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferences import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -53,15 +54,13 @@ class MangaPark(
private val apiUrl = "$baseUrl/apo/" private val apiUrl = "$baseUrl/apo/"
override val client = network.cloudflareClient.newBuilder().apply { private val json: Json by injectLazy()
if (preference.getBoolean(ENABLE_NSFW, true)) {
addInterceptor(::siteSettingsInterceptor) override val client = network.cloudflareClient.newBuilder()
addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2")) .addInterceptor(::siteSettingsInterceptor)
} .addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
rateLimitHost(apiUrl.toHttpUrl(), 1) .rateLimitHost(apiUrl.toHttpUrl(), 1)
// intentionally after rate limit interceptor so thumbnails are not rate limited .build()
addInterceptor(::thumbnailDomainInterceptor)
}.build()
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
@ -97,10 +96,8 @@ class MangaPark(
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<SearchResponse>() val result = response.parseAs<SearchResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
val entries = result.data.searchComics.items.map { it.data.toSManga(shortenTitle, pageAsCover) } val entries = result.data.searchComics.items.map { it.data.toSManga() }
val hasNextPage = entries.size == size val hasNextPage = entries.size == size
return MangasPage(entries, hasNextPage) return MangasPage(entries, hasNextPage)
@ -167,10 +164,8 @@ class MangaPark(
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<DetailsResponse>() val result = response.parseAs<DetailsResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
return result.data.comic.data.toSManga(shortenTitle, pageAsCover) return result.data.comic.data.toSManga()
} }
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#") override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
@ -225,7 +220,7 @@ class MangaPark(
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, _ -> setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, "Restart the app to apply changes", Toast.LENGTH_LONG).show() Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
true true
} }
}.also(screen::addPreference) }.also(screen::addPreference)
@ -236,34 +231,16 @@ class MangaPark(
summary = "Refresh chapter list to apply changes" summary = "Refresh chapter list to apply changes"
setDefaultValue(false) setDefaultValue(false)
}.also(screen::addPreference) }.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = ENABLE_NSFW
title = "Enable NSFW content"
summary = "Clear Cookies & Restart the app to apply changes."
setDefaultValue(true)
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHORTEN_TITLE_PREF
title = "Remove extra information from title"
summary = "Clear database to apply changes\n\n" +
"Note: doesn't not work for entries in library"
setDefaultValue(false)
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = UNCENSORED_COVER_PREF
title = "Attempt to use Uncensored Cover for Hentai"
summary = "Uses first or last chapter page as cover"
entries = arrayOf("Off", "First Chapter", "Last Chapter")
entryValues = arrayOf("off", "first", "last")
setDefaultValue("off")
}.also(screen::addPreference)
} }
private inline fun <reified T> Response.parseAs(): T =
use { body.string() }.let(json::decodeFromString)
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
filterIsInstance<T>().firstOrNull()
private inline fun <reified T : Any> T.toJsonRequestBody() = private inline fun <reified T : Any> T.toJsonRequestBody() =
toJsonString().toRequestBody(JSON_MEDIA_TYPE) json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true) private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1) private val latch = CountDownLatch(1)
@ -294,25 +271,6 @@ class MangaPark(
return chain.proceed(request) return chain.proceed(request)
} }
private fun thumbnailDomainInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
return if (url.host == THUMBNAIL_LOOPBACK_HOST) {
val newUrl = url.newBuilder()
.host(domain)
.build()
val newRequest = request.newBuilder()
.url(newUrl)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(request)
}
}
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
@ -340,11 +298,6 @@ class MangaPark(
"mpark.to", "mpark.to",
) )
private const val ENABLE_NSFW = "pref_nsfw"
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters" private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
private const val SHORTEN_TITLE_PREF = "pref_shorten_title"
private const val UNCENSORED_COVER_PREF = "pref_uncensored_cover"
} }
} }
const val THUMBNAIL_LOOPBACK_HOST = "127.0.0.1"

View File

@ -38,68 +38,33 @@ class MangaParkComic(
private val originalStatus: String? = null, private val originalStatus: String? = null,
private val uploadStatus: String? = null, private val uploadStatus: String? = null,
private val summary: String? = null, private val summary: String? = null,
private val extraInfo: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null, @SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String, private val urlPath: String,
@SerialName("max_chapterNode") private val latestChapter: Data<ImageFiles>? = null,
@SerialName("first_chapterNode") private val firstChapter: Data<ImageFiles>? = null,
) { ) {
fun toSManga(shortenTitle: Boolean, pageAsCover: String) = SManga.create().apply { fun toSManga() = SManga.create().apply {
url = "$urlPath#$id" url = "$urlPath#$id"
title = if (shortenTitle) { title = name
var shortName = name thumbnail_url = cover
while (shortenTitleRegex.containsMatchIn(shortName)) {
shortName = shortName.replace(shortenTitleRegex, "").trim()
}
shortName
} else {
name
}
thumbnail_url = run {
val coverUrl = cover?.let {
when {
it.startsWith("http") -> it
it.startsWith("/") -> "https://$THUMBNAIL_LOOPBACK_HOST$it"
else -> null
}
}
if (pageAsCover != "off" && useLatestPageAsCover(genres)) {
if (pageAsCover == "first") {
firstChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
} else {
latestChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
}
} else {
coverUrl
}
}
author = authors?.joinToString() author = authors?.joinToString()
artist = artists?.joinToString() artist = artists?.joinToString()
description = buildString { description = buildString {
if (shortenTitle) { val desc = summary?.let { Jsoup.parse(it).text() }
append(name) val names = altNames?.takeUnless { it.isEmpty() }
append("\n\n") ?.joinToString("\n") { "${it.trim()}" }
if (desc.isNullOrEmpty()) {
if (!names.isNullOrEmpty()) {
append("Alternative Names:\n", names)
}
} else {
append(desc)
if (!names.isNullOrEmpty()) {
append("\n\nAlternative Names:\n", names)
}
} }
summary?.also { }
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
extraInfo?.takeUnless(String::isBlank)?.also {
append("Extra Info:\n")
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
altNames?.takeUnless(List<String>::isEmpty)
?.joinToString(
prefix = "Alternative Names:\n",
separator = "\n",
) { "${it.trim()}" }
?.also(::append)
}.trim()
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() } genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
status = when (originalStatus ?: uploadStatus) { status = when (originalStatus) {
"ongoing" -> SManga.ONGOING "ongoing" -> SManga.ONGOING
"completed" -> { "completed" -> {
if (uploadStatus == "ongoing") { if (uploadStatus == "ongoing") {
@ -131,14 +96,6 @@ class MangaParkComic(
} }
return result.toString() return result.toString()
} }
private fun useLatestPageAsCover(genres: List<String>?): Boolean {
return genres.orEmpty().let {
it.contains("hentai") && !it.contains("webtoon")
}
}
private val shortenTitleRegex = Regex("""^(\[[^]]+\])|^(\([^)]+\))|^(\{[^}]+\})|(\[[^]]+\])${'$'}|(\([^)]+\))${'$'}|(\{[^}]+\})${'$'}""")
} }
} }

View File

@ -25,23 +25,8 @@ val SEARCH_QUERY = buildQuery {
originalStatus originalStatus
uploadStatus uploadStatus
summary summary
extraInfo
urlCoverOri urlCoverOri
urlPath urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
} }
} }
} }
@ -67,23 +52,8 @@ val DETAILS_QUERY = buildQuery {
originalStatus originalStatus
uploadStatus uploadStatus
summary summary
extraInfo
urlCoverOri urlCoverOri
urlPath urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
} }
} }
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Manhwa18CcFactory' extClass = '.Manhwa18CcFactory'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://manhwa18.cc' baseUrl = 'https://manhwa18.cc'
overrideVersionCode = 6 overrideVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl.Companion.toHttpUrl import eu.kanade.tachiyomi.source.model.Page
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -27,7 +28,7 @@ class Manhwa18CcEN : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "en") {
class Manhwa18CcKO : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "ko") { class Manhwa18CcKO : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "ko") {
override fun popularMangaSelector() = "div.manga-item:has(h3 a[title$='Raw'])" override fun popularMangaSelector() = "div.manga-item:has(h3 a[title$='Raw'])"
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page", headers) override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page")
} }
abstract class Manhwa18Cc( abstract class Manhwa18Cc(
@ -44,9 +45,9 @@ abstract class Manhwa18Cc(
override fun popularMangaNextPageSelector() = "ul.pagination li.next a" override fun popularMangaNextPageSelector() = "ul.pagination li.next a"
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending", headers) override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page", headers) override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page")
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
@ -59,12 +60,7 @@ abstract class Manhwa18Cc(
// "No results found" message. So this fix redirect to popular page. // "No results found" message. So this fix redirect to popular page.
if (query.isBlank()) return popularMangaRequest(page) if (query.isBlank()) return popularMangaRequest(page)
val url = "$baseUrl/search".toHttpUrl().newBuilder() return GET("$baseUrl/search?q=$query&page=$page")
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
} }
override val mangaSubString = "webtoon" override val mangaSubString = "webtoon"
@ -76,4 +72,16 @@ abstract class Manhwa18Cc(
override fun chapterDateSelector() = "span.chapter-time" override fun chapterDateSelector() = "span.chapter-time"
override val pageListParseSelector = "div.read-content img" override val pageListParseSelector = "div.read-content img"
override fun pageListParse(document: Document): List<Page> {
return document.select(pageListParseSelector).mapIndexed { index, element ->
Page(
index,
document.location(),
element?.let {
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
},
)
}
}
} }

View File

@ -1,8 +0,0 @@
ext {
extName = 'MissKon'
extClass = '.MissKon'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More