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
- label: I have updated all installed extensions.
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
- 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

View File

@ -1,18 +1,14 @@
# Keiyoushi Extensions
### 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) |
## 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.**
# Usage
* You can add our repo by visiting the [Keiyoushi Website](https://keiyoushi.github.io/add-repo)
* Otherwise, copy & paste the following URL: https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json
[Getting started](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo)
## Requests
# Requests
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!
Issues are up-for-grabs for any developer if there is no assigned user already.
## Contributing
# Contributing
Contributions are welcome!

View File

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

View File

@ -2,4 +2,4 @@ plugins {
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

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.content.ActivityNotFoundException
@ -10,7 +10,7 @@ import kotlin.system.exitProcess
/*
Springboard that accepts https://<domain>/truyen-tranh/$id/ intents
*/
class TeamLanhLungUrlActivity : Activity() {
class A3MangaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
@ -25,10 +25,10 @@ class TeamLanhLungUrlActivity : Activity() {
},
)
} catch (e: ActivityNotFoundException) {
Log.e("TeamLanhLungUrlActivity", e.toString())
Log.e("A3MangaThemeUrlActivity", e.toString())
}
} else {
Log.e("TeamLanhLungUrlActivity", "Could not parse URI from intent $intent")
Log.e("A3MangaThemeUrlActivity", "Could not parse URI from intent $intent")
}
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")
}
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.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -74,10 +73,10 @@ abstract class GroupLe(
override fun latestUpdatesSelector() = popularMangaSelector()
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 =
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 {
val manga = SManga.create()
@ -104,73 +103,15 @@ abstract class GroupLe(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search/advancedResults?offset=${50 * (page - 1)}"
.toHttpUrl()
.newBuilder()
val url =
"$baseUrl/search/advancedResults?offset=${50 * (page - 1)}".toHttpUrl()
.newBuilder()
if (query.isNotEmpty()) {
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)
}
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 {
val infoElement = document.select(".expandable").first()!!
val rawCategory = infoElement.select("span.elem_category").text()

View File

@ -2,4 +2,4 @@ plugins {
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 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 data = chapterResponse.parseAs<Post<ChapterListResponse>>()

View File

@ -2,4 +2,4 @@ plugins {
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.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.double
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class KemonoFavouritesDto(
val id: String,
@ -27,7 +25,7 @@ class KemonoCreatorDto(
) {
var fav: Long = 0
val updatedDate get() = when {
updated.isString -> dateFormat.tryParse(updated.content)
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
else -> (updated.double * 1000).toLong()
}
@ -64,7 +62,7 @@ class KemonoPostDto(
private val service: String,
private val user: String,
private val title: String,
private val added: String?,
private val added: String,
private val published: String?,
private val edited: String?,
private val file: KemonoFileDto,
@ -82,13 +80,13 @@ class KemonoPostDto(
}.distinctBy { it.path }.map { it.toString() }
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"
date_upload = postDate
date_upload = postDate?.time ?: 0
name = title.ifBlank {
val postDateString = when {
postDate != 0L -> chapterNameDateFormat.format(postDate)
postDate != null && postDate.time != 0L -> chapterNameDateFormat.format(postDate)
else -> "unknown date"
}

View File

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

View File

@ -59,16 +59,8 @@ abstract class Keyoapp(
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
open val popularMangaTitleSelector = listOf(
"Popular",
"Popularie",
"Trending",
)
override fun popularMangaSelector(): String = selector(
"div:contains(%s) + div .group.overflow-hidden.grid",
popularMangaTitleSelector,
)
override fun popularMangaSelector(): String =
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.getImageUrl("*[style*=background-image]")
@ -251,7 +243,7 @@ abstract class Keyoapp(
override fun chapterListSelector(): String {
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)))"
}
@ -365,10 +357,6 @@ abstract class Keyoapp(
return now.timeInMillis
}
private fun selector(selector: String, contains: List<String>): String {
return contains.joinToString { selector.replace("%s", it) }
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_PAID_CHAPTERS_PREF

View File

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

View File

@ -245,13 +245,11 @@ abstract class Madara(
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(mangaSubString)
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
addPathSegment("") // add trailing slash
}.build()
return client.newCall(GET(mangaUrl, headers))
.asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply {
setUrlWithoutDomain(mangaUrl.toString())
initialized = true
}
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 chapterProtectorSelector = "#chapter-protector-data"
open val chapterProtectorPasswordPrefix = "wpmangaprotectornonce='"
open val chapterProtectorDataPrefix = "chapter_data='"
override fun pageListParse(document: Document): List<Page> {
launchIO { countViews(document) }
@ -996,11 +992,11 @@ abstract class Madara(
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
?: chapterProtector.html()
val password = chapterProtectorHtml
.substringAfter(chapterProtectorPasswordPrefix)
.substringAfter("wpmangaprotectornonce='")
.substringBefore("';")
val chapterData = json.parseToJsonElement(
chapterProtectorHtml
.substringAfter(chapterProtectorDataPrefix)
.substringAfter("chapter_data='")
.substringBefore("';")
.replace("\\/", "/"),
).jsonObject

View File

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

View File

@ -85,9 +85,6 @@ abstract class MangaBox(
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (cdnSet.isEmpty()) {
return chain.proceed(request)
}
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
val originalResponse: Response? = try {
chain.proceed(request)
@ -349,10 +346,11 @@ abstract class MangaBox(
}
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 =
extractArray(content, "cdns") + extractArray(content, "backupImage")
val chapterImages = extractArray(content, "chapterImages")
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
val chapterImages = extractArray(element.html(), "chapterImages")
// Add all parsed cdns to set
cdnSet.addAll(cdns)
@ -371,10 +369,6 @@ abstract class MangaBox(
}
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")
}
baseVersionCode = 34
baseVersionCode = 30
dependencies {
//noinspection UseTomlInstead
implementation("org.brotli:dec:0.1.2")
api(project(":lib:randomua"))
}

View File

@ -1,71 +1,62 @@
package eu.kanade.tachiyomi.multisrc.mangahub
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
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.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.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 keiyoushi.utils.tryParse
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.brotli.dec.BrotliInputStream
import java.io.ByteArrayOutputStream
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.net.URLEncoder
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.GZIPInputStream
import kotlin.random.Random
abstract class MangaHub(
override val name: String,
final override val baseUrl: String,
override val lang: String,
private val mangaSource: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH),
) : HttpSource(), ConfigurableSource {
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MM-dd-yyyy", Locale.US),
) : ParsedHttpSource() {
override val supportsLatest = true
private val baseApiUrl = "https://api.mghcdn.com"
private val baseCdnUrl = "https://imgx.mghcdn.com"
private val baseThumbCdnUrl = "https://thumb.mghcdn.com"
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,
)
private var baseApiUrl = "https://api.mghcdn.com"
private var baseCdnUrl = "https://imgx.mghcdn.com"
private val regex = Regex("mhub_access=([^;]+)")
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
userAgentType = UserAgentType.DESKTOP,
filterInclude = listOf("chrome"),
)
.addInterceptor(::apiAuthInterceptor)
.addNetworkInterceptor(::compatEncodingInterceptor)
.rateLimit(1)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
@ -78,158 +69,60 @@ abstract class MangaHub(
.add("Sec-Fetch-Site", "same-origin")
.add("Upgrade-Insecure-Requests", "1")
private fun postRequestGraphQL(query: String, refreshUrl: String? = null): Request {
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()
}
open val json: Json by injectLazy()
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val tag = request.tag(GraphQLTag::class.java)
?: return chain.proceed(request) // We won't intercept non-graphql requests (like image retrieval)
val originalRequest = chain.request()
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
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
?: throw MangaHubCookieNotFound()
val apiRequest = request.newBuilder()
.header("x-mhub-access", cookie.value)
.build()
val request =
if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
originalRequest.newBuilder()
.header("x-mhub-access", cookie.value)
.build()
} else {
originalRequest
}
val response = chain.proceed(apiRequest)
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
return chain.proceed(request)
}
private class MangaHubCookieNotFound : IOException("mhub_access cookie not found")
private class ApiErrorException(errorMessage: String) : IOException(errorMessage)
private fun refreshApiKey(chapter: SChapter) {
val slug = "$baseUrl${chapter.url}"
.toHttpUrlOrNull()
?.pathSegments
?.get(1)
private val lock = ReentrantLock()
private var refreshed = 0L
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()
val url = if (slug != null) {
"$baseUrl/manga/$slug".toHttpUrl()
} else {
lock.lock() // wait here until lock is released
lock.unlock()
baseUrl.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))
// 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,
)
private fun ApiMangaSearchItem.toSignature(): String {
val author = this.author
val chNum = this.latestChapter
val genres = this.genres
private fun Element.toSignature(): String {
val author = this.select("small").text()
val chNum = this.select(".col-sm-6 a:contains(#)").text()
val genres = this.select(".genre-label").joinToString { it.text() }
return author + chNum + genres
}
private fun mangaRequest(page: Int, order: String): Request {
return postRequestGraphQL(searchQuery(mangaSource, "", "all", order, page))
}
// 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
// titles, URLs, and image names. in order to cut these "duplicates" down,
// 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
override fun popularMangaParse(response: Response): MangasPage {
val mangaList = response.parseAs<ApiSearchResponse>()
val doc = response.asJsoup()
val mangas = mangaList.data.search.rows.map {
SMangaDTO(
"$baseUrl/manga/${it.slug}",
it.title,
"$baseThumbCdnUrl/${it.image}",
it.toSignature(),
)
}
val mangas = doc.select(popularMangaSelector())
.map {
SMangaDTO(
it.select("h4 a").attr("abs:href"),
it.select("h4 a").text(),
it.select("img").attr("abs:src"),
it.toSignature(),
)
}
.distinctBy { it.signature }
.map {
SManga.create().apply {
@ -278,171 +170,221 @@ abstract class MangaHub(
thumbnail_url = it.thumbnailUrl
}
}
// Entries have a max of 30 per request
return MangasPage(mangas, mangaList.data.search.rows.count() == 30)
return MangasPage(mangas, doc.select(popularMangaNextPageSelector()).isNotEmpty())
}
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
override fun latestUpdatesRequest(page: Int): Request {
return mangaRequest(page, "LATEST")
return GET("$baseUrl/updates/page/$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
return popularMangaParse(response)
}
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var order = "POPULAR"
var genres = "all"
val url = "$baseUrl/search/page/$page".toHttpUrl().newBuilder()
url.addQueryParameter("q", query)
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is OrderBy -> {
order = filter.values[filter.state].key
val order = filter.values[filter.state]
url.addQueryParameter("order", order.key)
}
is GenreList -> {
genres = filter.included.joinToString(",").takeIf { it.isNotBlank() } ?: "all"
val genre = filter.values[filter.state]
url.addQueryParameter("genre", genre.key)
}
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 {
return popularMangaParse(response)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// manga details
override fun mangaDetailsRequest(manga: SManga): Request {
return postRequestGraphQL(
mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")),
refreshUrl = "$baseUrl${manga.url}",
)
}
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".breadcrumb .active span").text()
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 {
val rawManga = response.parseAs<ApiMangaDetailsResponse>()
return SManga.create().apply {
title = rawManga.data.manga.title!!
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
document.select("div:has(h1) span:contains(Status) + span").first()?.text()?.also { statusText ->
when {
statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
else -> manga.status = SManga.UNKNOWN
}
}
description = buildString {
rawManga.data.manga.description?.let(::append)
// Add alternative title
val altTitle = rawManga.data.manga.alternativeTitle
if (!altTitle.isNullOrBlank()) {
if (isNotBlank()) append("\n\n")
append("Alternative Name: $altTitle")
// add alternative name to manga description
document.select("h1 small").firstOrNull()?.ownText()?.let { alternativeName ->
if (alternativeName.isNotBlank()) {
manga.description = manga.description.orEmpty().let {
if (it.isBlank()) {
"Alternative Name: $alternativeName"
} else {
"$it\n\nAlternative Name: $alternativeName"
}
}
}
}
return manga
}
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
// Chapters
override fun chapterListRequest(manga: SManga): Request {
return postRequestGraphQL(
mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")),
refreshUrl = "$baseUrl${manga.url}",
)
}
// chapters
override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = response.parseAs<ApiMangaDetailsResponse>()
val useGenericTitle = preferences.getUseGenericTitlePref()
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
val document = response.asJsoup()
val head = document.head()
return document.select(chapterListSelector()).map { chapterFromElement(it, head) }
}
private fun generateChapterName(title: String, number: String): String {
return if (title.contains(number)) {
title
} else if (title.isNotBlank()) {
"Chapter $number - $title"
} else {
generateGenericChapterName(number)
override fun chapterListSelector() = ".tab-content ul li"
private fun chapterFromElement(element: Element, head: Element): SChapter {
val chapter = SChapter.create()
val potentialLinks = element.select("a[href*='$baseUrl/chapter/']:not([rel*=nofollow]):not([rel*=noreferrer])")
var visibleLink = ""
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 {
return "Chapter $number"
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
}
override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/chapter${chapter.url}"
// Pages
override fun pageListRequest(chapter: SChapter): Request {
val chapterUrl = chapter.url.split("/")
return postRequestGraphQL(
pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()),
refreshUrl = "$baseUrl/chapter${chapter.url}",
)
}
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)
private fun parseChapterDate(date: String): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
when {
"just now" in date || "less than an hour" in date -> {
parsedDate = now.timeInMillis
}
}.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()
.domain(baseHttpUrl.host)
.name("recently")
.value(URLEncoder.encode(recently, "utf-8"))
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
// pages
override fun pageListRequest(chapter: SChapter): Request {
val body = buildJsonObject {
put("query", PAGES_QUERY)
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()
// Add/update the cookie
client.cookieJar.saveFromResponse(baseHttpUrl, listOf(recentlyCookie))
return POST("$baseApiUrl/graphql", newHeaders, body)
}
// We'll log our action to the site to further increase the chance of valid API key
val ipRequest = client.newCall(GET("https://api.ipify.org?format=json")).execute()
val ip = ipRequest.parseAs<PublicIPResponse>().ip
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
super.fetchPageList(chapter)
.doOnError { refreshApiKey(chapter) }
.retry(1)
client.newCall(GET("$baseUrl/action/logHistory2/${chapterObject.data.chapter.manga.slug}/${chapterObject.data.chapter.chapterNumber}?browserID=$ip")).execute().close()
ipRequest.close()
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> {
val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
return pages.images.mapIndexed { i, page ->
Page(i, "", "$baseCdnUrl/${pages.page}$page")
if (chapterObject.data?.chapter == null) {
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)
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// filters
private class Genre(title: String, val key: String) : Filter.CheckBox(title) {
fun getGenreKey(): String {
return key
}
private class Genre(title: String, val key: String) : Filter.TriState(title) {
override fun toString(): String {
return name
}
@ -479,14 +417,11 @@ abstract class MangaHub(
}
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) {
val included: List<String>
get() = state.filter { it.state }.map { it.getGenreKey() }
}
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Genres", genres, 0)
override fun getFilterList() = FilterList(
GenreList(genres),
OrderBy(orderBy),
GenreList(genres),
)
private val orderBy = arrayOf(
@ -497,119 +432,70 @@ abstract class MangaHub(
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("Adventure", "adventure"),
Genre("Award Winning", "award-winning"),
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("Horror", "horror"),
Genre("Tragedy", "tragedy"),
Genre("Crime", "crime"),
Genre("Demons", "demons"),
Genre("Doujinshi", "doujinshi"),
Genre("Sci-Fi", "sci-fi"),
Genre("Yuri", "yuri"),
Genre("Yaoi", "yaoi"),
Genre("Shoujo", "shoujo"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
Genre("Food", "food"),
Genre("Game", "game"),
Genre("Gender bender", "gender-bender"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Isekai", "isekai"),
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("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("Music", "music"),
Genre("Mystery", "mystery"),
Genre("One shot", "one-shot"),
Genre("Oneshot", "oneshot"),
Genre("Parody", "parody"),
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("Food", "food"),
Genre("Kids", "kids"),
Genre("Magical Girls", "magical-girls"),
Genre("Wuxia", "wuxia"),
Genre("Superhero", "superhero"),
Genre("Supernatural", "supernatural"),
Genre("Thriller", "thriller"),
Genre("Crime", "crime"),
Genre("Philosophical", "philosophical"),
Genre("Adaptation", "adaptation"),
Genre("Full Color", "full-color"),
Genre("Crossdressing", "crossdressing"),
Genre("Reincarnation", "reincarnation"),
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"
}
Genre("Tragedy", "tragedy"),
Genre("Vampire", "vampire"),
Genre("Webtoon", "webtoon"),
Genre("Webtoons", "webtoons"),
Genre("Wuxia", "wuxia"),
Genre("Yuri", "yuri"),
)
}

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
class GraphQLTag(
val refreshUrl: String? = null,
import kotlinx.serialization.Serializable
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 ->
"""
{
search(x: $mangaSource, q: "$query", genre: "$genre", mod: $order, offset: ${(page - 1) * 30}) {
rows {
title,
author,
slug,
image,
genres,
latestChapter
}
}
}
""".trimIndent()
}
@Serializable
data class ApiChapterPagesResponse(
val data: ApiChapterData?,
val errors: List<ApiErrorMessages>?,
)
val mangaDetailsQuery = { mangaSource: String, slug: String ->
"""
{
manga(x: $mangaSource, slug: "$slug") {
title,
slug,
status,
image,
author,
artist,
genres,
description,
alternativeTitle
}
}
""".trimIndent()
}
@Serializable
data class ApiChapterData(
val chapter: ApiChapter?,
)
val mangaChapterListQuery = { mangaSource: String, slug: String ->
"""
{
manga(x: $mangaSource, slug: "$slug") {
slug,
chapters {
number,
title,
date
}
}
}
""".trimIndent()
}
@Serializable
data class ApiChapter(
val pages: String,
)
val pagesQuery = { mangaSource: String, slug: String, number: Float ->
"""
{
chapter(x: $mangaSource, slug: "$slug", number: $number) {
pages,
mangaID,
number,
manga {
slug
}
}
}
""".trimIndent()
}
@Serializable
data class ApiChapterPages(
val p: String,
val i: List<String>,
)

View File

@ -2,4 +2,4 @@ plugins {
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 {
val request = chain.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
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 {
extName = 'Bato.to'
extClass = '.BatoToFactory'
extVersionCode = 50
extVersionCode = 49
isNsfw = true
}

View File

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

View File

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

View File

@ -1,9 +1,6 @@
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.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class BuonDua() : ParsedHttpSource() {
override val baseUrl = "https://buondua.com"
@ -27,13 +20,6 @@ class BuonDua() : ParsedHttpSource() {
override val name = "Buon Dua"
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
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
@ -57,10 +43,10 @@ class BuonDua() : ParsedHttpSource() {
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/hot?start=${20 * (page - 1)}")
}
override fun popularMangaSelector() = latestUpdatesSelector()
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -71,7 +57,6 @@ class BuonDua() : ParsedHttpSource() {
else -> popularMangaRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
@ -87,27 +72,34 @@ class BuonDua() : ParsedHttpSource() {
return manga
}
override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1
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 chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select(".is-current").first()!!.attr("abs:href"))
chapter.chapter_number = 0F
chapter.name = element.select(".article-header").text()
chapter.date_upload = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US).parse(element.select(".article-info > small").text())?.time ?: 0L
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select(".article-fulltext img")
.mapIndexed { i, imgEl -> Page(i, imageUrl = imgEl.absUrl("src")) }
val numpages = document.selectFirst(".pagination-list")!!.select(".pagination-link")
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()
@ -122,8 +114,4 @@ class BuonDua() : ParsedHttpSource() {
class TagFilter : Filter.Text("Tag ID")
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_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_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ ext {
extClass = '.EternalMangasFactory'
themePkg = 'mangaesp'
baseUrl = 'https://eternalmangas.com'
overrideVersionCode = 5
overrideVersionCode = 2
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.SeriesDto
import eu.kanade.tachiyomi.network.GET
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.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -34,58 +26,16 @@ open class EternalMangas(
) {
override val useApiSearch = true
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("views", false))
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("updated_at", false))
override fun latestUpdatesParse(response: Response): MangasPage {
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 List<SeriesDto>.additionalParse(): List<SeriesDto> {
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 {
val body = jsRedirect(response)
@ -142,7 +92,7 @@ open class EternalMangas(
private fun jsRedirect(response: Response): String {
var body = response.body.string()
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 inputs = it.select("input")
@ -156,13 +106,8 @@ open class EternalMangas(
return body
}
private fun createSortFilter(value: String, ascending: Boolean = false): FilterList {
val sortProperties = getSortProperties()
val index = sortProperties.indexOfFirst { it.value == value }.takeIf { it >= 0 } ?: 0
return FilterList(
SortByFilter("", sortProperties).apply {
state = Filter.Sort.Selection(index, ascending)
},
)
}
@Serializable
class LatestUpdatesDto(
val updates: Map<String, List<List<SeriesDto>>>,
)
}

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 {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 39
extVersionCode = 38
isNsfw = true
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
ext {
extName = 'SchaleNetwork'
extClass = '.KoharuFactory'
extVersionCode = 14
extVersionCode = 13
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.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
@ -122,9 +120,7 @@ class TurnstileInterceptor(
try {
val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders(authHeader)
val response = runBlocking(Dispatchers.IO) {
noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
}
val response = noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
response.use {
if (response.isSuccessful) {
with(response) {
@ -180,9 +176,7 @@ class TurnstileInterceptor(
try {
val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders("Bearer $token")
val response = runBlocking(Dispatchers.IO) {
noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
}
val response = noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
response.use {
if (response.isSuccessful) {
return true

View File

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

View File

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

View File

@ -38,7 +38,7 @@ class SeriesDto(
metadata.status == "HIATUS" -> SManga.ON_HIATUS
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 }
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
author = map["writer"]?.distinct()?.joinToString()

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
ext {
extName = 'Manga Hosted'
extClass = '.MangaHostedFactory'
extVersionCode = 3
extVersionCode = 2
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 baseUrl: String = "https://mangago.fit/${langOption.infix}"
override val baseUrl: String = "https://mangahosted.org"
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> {
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))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
@ -184,7 +184,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${dto.slug}"
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
@ -195,7 +195,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.mangago.fit"
val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api"
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("it", "manga-italia", "manga-it"),
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-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaPark'
extClass = '.MangaParkFactory'
extVersionCode = 22
extVersionCode = 21
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
@ -53,15 +54,13 @@ class MangaPark(
private val apiUrl = "$baseUrl/apo/"
override val client = network.cloudflareClient.newBuilder().apply {
if (preference.getBoolean(ENABLE_NSFW, true)) {
addInterceptor(::siteSettingsInterceptor)
addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
}
rateLimitHost(apiUrl.toHttpUrl(), 1)
// intentionally after rate limit interceptor so thumbnails are not rate limited
addInterceptor(::thumbnailDomainInterceptor)
}.build()
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::siteSettingsInterceptor)
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
.rateLimitHost(apiUrl.toHttpUrl(), 1)
.build()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
@ -97,10 +96,8 @@ class MangaPark(
override fun searchMangaParse(response: Response): MangasPage {
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
return MangasPage(entries, hasNextPage)
@ -167,10 +164,8 @@ class MangaPark(
override fun mangaDetailsParse(response: Response): SManga {
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("#")
@ -225,7 +220,7 @@ class MangaPark(
summary = "%s"
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
}
}.also(screen::addPreference)
@ -236,34 +231,16 @@ class MangaPark(
summary = "Refresh chapter list to apply changes"
setDefaultValue(false)
}.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() =
toJsonString().toRequestBody(JSON_MEDIA_TYPE)
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1)
@ -294,25 +271,6 @@ class MangaPark(
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 {
throw UnsupportedOperationException()
}
@ -340,11 +298,6 @@ class MangaPark(
"mpark.to",
)
private const val ENABLE_NSFW = "pref_nsfw"
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 uploadStatus: String? = null,
private val summary: String? = null,
private val extraInfo: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null,
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"
title = if (shortenTitle) {
var shortName = name
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
}
}
title = name
thumbnail_url = cover
author = authors?.joinToString()
artist = artists?.joinToString()
description = buildString {
if (shortenTitle) {
append(name)
append("\n\n")
val desc = summary?.let { Jsoup.parse(it).text() }
val names = altNames?.takeUnless { it.isEmpty() }
?.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() }
status = when (originalStatus ?: uploadStatus) {
status = when (originalStatus) {
"ongoing" -> SManga.ONGOING
"completed" -> {
if (uploadStatus == "ongoing") {
@ -131,14 +96,6 @@ class MangaParkComic(
}
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
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}
@ -67,23 +52,8 @@ val DETAILS_QUERY = buildQuery {
originalStatus
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Manhwa18CcFactory'
themePkg = 'madara'
baseUrl = 'https://manhwa18.cc'
overrideVersionCode = 6
overrideVersionCode = 5
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.SourceFactory
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl.Companion.toHttpUrl
import eu.kanade.tachiyomi.source.model.Page
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
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") {
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(
@ -44,9 +45,9 @@ abstract class Manhwa18Cc(
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()
@ -59,12 +60,7 @@ abstract class Manhwa18Cc(
// "No results found" message. So this fix redirect to popular page.
if (query.isBlank()) return popularMangaRequest(page)
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
return GET("$baseUrl/search?q=$query&page=$page")
}
override val mangaSubString = "webtoon"
@ -76,4 +72,16 @@ abstract class Manhwa18Cc(
override fun chapterDateSelector() = "span.chapter-time"
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