Compare commits
No commits in common. "c66abf25b95e984663af0fcc0ed5cf6586fed3d4" and "07eba2f8f2890c1aa39196ce16d4fb771c5745c6" have entirely different histories.
c66abf25b9
...
07eba2f8f2
@ -1,2 +0,0 @@
|
|||||||
# Force \n line ending, trailing newline
|
|
||||||
b72776e1f528c56ae6a9ef3a6f8d72533b00d39f
|
|
2
.github/ISSUE_TEMPLATE/01_report_issue.yml
vendored
@ -99,7 +99,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
|
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
|
14
README.md
@ -1,18 +1,14 @@
|
|||||||
# Keiyoushi Extensions
|
|
||||||
|
|
||||||
### Please give the repo a :star:
|
### Please give the repo a :star:
|
||||||
|
|
||||||
| Build | Need Help? |
|
| Build | Support Server |
|
||||||
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml) | [](https://discord.gg/3FbCpdKbdY) |
|
| [](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml) | [](https://discord.gg/3FbCpdKbdY) |
|
||||||
|
|
||||||
## Usage
|
# Usage
|
||||||
**If you are new to repository/extensions, please read the [Keiyoushi Getting Started guide](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo) first.**
|
|
||||||
|
|
||||||
* You can add our repo by visiting the [Keiyoushi Website](https://keiyoushi.github.io/add-repo)
|
[Getting started](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo)
|
||||||
* Otherwise, copy & paste the following URL: https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json
|
|
||||||
|
|
||||||
## Requests
|
# Requests
|
||||||
|
|
||||||
To request a new source or bug fix, [create an issue](https://github.com/keiyoushi/extensions-source/issues/new/choose).
|
To request a new source or bug fix, [create an issue](https://github.com/keiyoushi/extensions-source/issues/new/choose).
|
||||||
|
|
||||||
@ -23,7 +19,7 @@ difficult to maintain.
|
|||||||
If you would like to see a request fulfilled and have the necessary skills to do so, consider contributing!
|
If you would like to see a request fulfilled and have the necessary skills to do so, consider contributing!
|
||||||
Issues are up-for-grabs for any developer if there is no assigned user already.
|
Issues are up-for-grabs for any developer if there is no assigned user already.
|
||||||
|
|
||||||
## Contributing
|
# Contributing
|
||||||
|
|
||||||
Contributions are welcome!
|
Contributions are welcome!
|
||||||
|
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".all.globalcomix.GlobalComixUrlActivity"
|
android:name="eu.kanade.tachiyomi.multisrc.a3manga.A3MangaUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
<intent-filter
|
<intent-filter>
|
||||||
android:autoVerify="false"
|
|
||||||
tools:targetApi="23">
|
|
||||||
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:host="globalcomix.com" />
|
<data android:host="${SOURCEHOST}" />
|
||||||
<data android:scheme="https" />
|
<data android:host="*.${SOURCEHOST}" />
|
||||||
|
<data android:pathPattern="/truyen-tranh/..*"
|
||||||
<data android:pathPattern="/c/..*" />
|
android:scheme="${SOURCESCHEME}" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.extension.vi.teamlanhlung
|
package eu.kanade.tachiyomi.multisrc.a3manga
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.extension.vi.teamlanhlung
|
package eu.kanade.tachiyomi.multisrc.a3manga
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@ -10,7 +10,7 @@ import kotlin.system.exitProcess
|
|||||||
/*
|
/*
|
||||||
Springboard that accepts https://<domain>/truyen-tranh/$id/ intents
|
Springboard that accepts https://<domain>/truyen-tranh/$id/ intents
|
||||||
*/
|
*/
|
||||||
class TeamLanhLungUrlActivity : Activity() {
|
class A3MangaUrlActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
@ -25,10 +25,10 @@ class TeamLanhLungUrlActivity : Activity() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("TeamLanhLungUrlActivity", e.toString())
|
Log.e("A3MangaThemeUrlActivity", e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("TeamLanhLungUrlActivity", "Could not parse URI from intent $intent")
|
Log.e("A3MangaThemeUrlActivity", "Could not parse URI from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 26 KiB |
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 33
|
baseVersionCode = 31
|
||||||
|
@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -74,10 +73,10 @@ abstract class GroupLe(
|
|||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
GET("$baseUrl/list?sortType=rate&offset=${50 * (page - 1)}", headers)
|
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}", headers)
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
GET("$baseUrl/list?sortType=updated&offset=${50 * (page - 1)}", headers)
|
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}", headers)
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
@ -104,73 +103,15 @@ abstract class GroupLe(
|
|||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/search/advancedResults?offset=${50 * (page - 1)}"
|
val url =
|
||||||
.toHttpUrl()
|
"$baseUrl/search/advancedResults?offset=${50 * (page - 1)}".toHttpUrl()
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
|
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
url.addQueryParameter("q", query)
|
url.addQueryParameter("q", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreList -> filter.state.forEach { genre ->
|
|
||||||
if (genre.state != Filter.TriState.STATE_IGNORE) {
|
|
||||||
url.addQueryParameter(genre.id, arrayOf("=", "=in", "=ex")[genre.state])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is CategoryList -> filter.state.forEach { category ->
|
|
||||||
if (category.state != Filter.TriState.STATE_IGNORE) {
|
|
||||||
url.addQueryParameter(category.id, arrayOf("=", "=in", "=ex")[category.state])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is AgeList -> filter.state.forEach { age ->
|
|
||||||
if (age.state != Filter.TriState.STATE_IGNORE) {
|
|
||||||
url.addQueryParameter(age.id, arrayOf("=", "=in", "=ex")[age.state])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is MoreList -> filter.state.forEach { more ->
|
|
||||||
if (more.state != Filter.TriState.STATE_IGNORE) {
|
|
||||||
url.addQueryParameter(more.id, arrayOf("=", "=in", "=ex")[more.state])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is AdditionalFilterList -> filter.state.forEach { fils ->
|
|
||||||
if (fils.state != Filter.TriState.STATE_IGNORE) {
|
|
||||||
url.addQueryParameter(fils.id, arrayOf("=", "=in", "=ex")[fils.state])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is OrderBy -> {
|
|
||||||
url.addQueryParameter(
|
|
||||||
"sortType",
|
|
||||||
arrayOf("RATING", "POPULARITY", "YEAR", "NAME", "DATE_CREATE", "DATE_UPDATE", "USER_RATING")[filter.state],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.toString().replace("=%3D", "="), headers)
|
return GET(url.toString().replace("=%3D", "="), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class OrderBy : Filter.Select<String>(
|
|
||||||
"Сортировка",
|
|
||||||
arrayOf("По популярности", "Популярно сейчас", "По году", "По алфавиту", "Новинки", "По дате обновления", "По рейтингу"),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected class Genre(name: String, val id: String) : Filter.TriState(name)
|
|
||||||
|
|
||||||
protected class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанры", genres)
|
|
||||||
protected class CategoryList(categories: List<Genre>) : Filter.Group<Genre>("Категории", categories)
|
|
||||||
protected class AgeList(ages: List<Genre>) : Filter.Group<Genre>("Возрастная рекомендация", ages)
|
|
||||||
protected class MoreList(moren: List<Genre>) : Filter.Group<Genre>("Прочее", moren)
|
|
||||||
protected class AdditionalFilterList(fils: List<Genre>) : Filter.Group<Genre>("Фильтры", fils)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val infoElement = document.select(".expandable").first()!!
|
val infoElement = document.select(".expandable").first()!!
|
||||||
val rawCategory = infoElement.select("span.elem_category").text()
|
val rawCategory = infoElement.select("span.elem_category").text()
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 9
|
baseVersionCode = 8
|
||||||
|
@ -127,7 +127,7 @@ abstract class Iken(
|
|||||||
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
|
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
|
||||||
|
|
||||||
val id = response.request.url.fragment!!
|
val id = response.request.url.fragment!!
|
||||||
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=900&order=desc&userid=$userId"
|
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
|
||||||
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
|
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
|
||||||
|
|
||||||
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
|
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 21
|
baseVersionCode = 20
|
||||||
|
@ -2,13 +2,11 @@ package eu.kanade.tachiyomi.multisrc.kemono
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import keiyoushi.utils.tryParse
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.double
|
import kotlinx.serialization.json.double
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoFavouritesDto(
|
class KemonoFavouritesDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
@ -27,7 +25,7 @@ class KemonoCreatorDto(
|
|||||||
) {
|
) {
|
||||||
var fav: Long = 0
|
var fav: Long = 0
|
||||||
val updatedDate get() = when {
|
val updatedDate get() = when {
|
||||||
updated.isString -> dateFormat.tryParse(updated.content)
|
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
||||||
else -> (updated.double * 1000).toLong()
|
else -> (updated.double * 1000).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +62,7 @@ class KemonoPostDto(
|
|||||||
private val service: String,
|
private val service: String,
|
||||||
private val user: String,
|
private val user: String,
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val added: String?,
|
private val added: String,
|
||||||
private val published: String?,
|
private val published: String?,
|
||||||
private val edited: String?,
|
private val edited: String?,
|
||||||
private val file: KemonoFileDto,
|
private val file: KemonoFileDto,
|
||||||
@ -82,13 +80,13 @@ class KemonoPostDto(
|
|||||||
}.distinctBy { it.path }.map { it.toString() }
|
}.distinctBy { it.path }.map { it.toString() }
|
||||||
|
|
||||||
fun toSChapter() = SChapter.create().apply {
|
fun toSChapter() = SChapter.create().apply {
|
||||||
val postDate = dateFormat.tryParse(edited ?: published ?: added)
|
val postDate = dateFormat.parse(edited ?: published ?: added)
|
||||||
|
|
||||||
url = "/$service/user/$user/post/$id"
|
url = "/$service/user/$user/post/$id"
|
||||||
date_upload = postDate
|
date_upload = postDate?.time ?: 0
|
||||||
name = title.ifBlank {
|
name = title.ifBlank {
|
||||||
val postDateString = when {
|
val postDateString = when {
|
||||||
postDate != 0L -> chapterNameDateFormat.format(postDate)
|
postDate != null && postDate.time != 0L -> chapterNameDateFormat.format(postDate)
|
||||||
else -> "unknown date"
|
else -> "unknown date"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 17
|
baseVersionCode = 15
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -59,16 +59,8 @@ abstract class Keyoapp(
|
|||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||||
|
|
||||||
open val popularMangaTitleSelector = listOf(
|
override fun popularMangaSelector(): String =
|
||||||
"Popular",
|
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
|
||||||
"Popularie",
|
|
||||||
"Trending",
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = selector(
|
|
||||||
"div:contains(%s) + div .group.overflow-hidden.grid",
|
|
||||||
popularMangaTitleSelector,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
thumbnail_url = element.getImageUrl("*[style*=background-image]")
|
thumbnail_url = element.getImageUrl("*[style*=background-image]")
|
||||||
@ -251,7 +243,7 @@ abstract class Keyoapp(
|
|||||||
|
|
||||||
override fun chapterListSelector(): String {
|
override fun chapterListSelector(): String {
|
||||||
if (!preferences.showPaidChapters) {
|
if (!preferences.showPaidChapters) {
|
||||||
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[alt~=Coin]))"
|
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[src*=Coin.svg]))"
|
||||||
}
|
}
|
||||||
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
|
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
|
||||||
}
|
}
|
||||||
@ -365,10 +357,6 @@ abstract class Keyoapp(
|
|||||||
return now.timeInMillis
|
return now.timeInMillis
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selector(selector: String, contains: List<String>): String {
|
|
||||||
return contains.joinToString { selector.replace("%s", it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = SHOW_PAID_CHAPTERS_PREF
|
key = SHOW_PAID_CHAPTERS_PREF
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 42
|
baseVersionCode = 41
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cryptoaes"))
|
api(project(":lib:cryptoaes"))
|
||||||
|
@ -245,13 +245,11 @@ abstract class Madara(
|
|||||||
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addPathSegment(mangaSubString)
|
addPathSegment(mangaSubString)
|
||||||
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
|
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
|
||||||
addPathSegment("") // add trailing slash
|
|
||||||
}.build()
|
}.build()
|
||||||
return client.newCall(GET(mangaUrl, headers))
|
return client.newCall(GET(mangaUrl, headers))
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val manga = mangaDetailsParse(response).apply {
|
val manga = mangaDetailsParse(response).apply {
|
||||||
setUrlWithoutDomain(mangaUrl.toString())
|
setUrlWithoutDomain(mangaUrl.toString())
|
||||||
initialized = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MangasPage(listOf(manga), false)
|
MangasPage(listOf(manga), false)
|
||||||
@ -979,8 +977,6 @@ abstract class Madara(
|
|||||||
open val pageListParseSelector = "div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img"
|
open val pageListParseSelector = "div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img"
|
||||||
|
|
||||||
open val chapterProtectorSelector = "#chapter-protector-data"
|
open val chapterProtectorSelector = "#chapter-protector-data"
|
||||||
open val chapterProtectorPasswordPrefix = "wpmangaprotectornonce='"
|
|
||||||
open val chapterProtectorDataPrefix = "chapter_data='"
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
launchIO { countViews(document) }
|
launchIO { countViews(document) }
|
||||||
@ -996,11 +992,11 @@ abstract class Madara(
|
|||||||
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
|
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
|
||||||
?: chapterProtector.html()
|
?: chapterProtector.html()
|
||||||
val password = chapterProtectorHtml
|
val password = chapterProtectorHtml
|
||||||
.substringAfter(chapterProtectorPasswordPrefix)
|
.substringAfter("wpmangaprotectornonce='")
|
||||||
.substringBefore("';")
|
.substringBefore("';")
|
||||||
val chapterData = json.parseToJsonElement(
|
val chapterData = json.parseToJsonElement(
|
||||||
chapterProtectorHtml
|
chapterProtectorHtml
|
||||||
.substringAfter(chapterProtectorDataPrefix)
|
.substringAfter("chapter_data='")
|
||||||
.substringBefore("';")
|
.substringBefore("';")
|
||||||
.replace("\\/", "/"),
|
.replace("\\/", "/"),
|
||||||
).jsonObject
|
).jsonObject
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 7
|
baseVersionCode = 6
|
||||||
|
@ -85,9 +85,6 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
|
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
if (cdnSet.isEmpty()) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
|
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
|
||||||
val originalResponse: Response? = try {
|
val originalResponse: Response? = try {
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
@ -349,10 +346,11 @@ abstract class MangaBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val content = document.select("script:containsData(cdns =)").joinToString("\n") { it.data() }
|
val element = document.select("head > script").lastOrNull()
|
||||||
|
?: return emptyList()
|
||||||
val cdns =
|
val cdns =
|
||||||
extractArray(content, "cdns") + extractArray(content, "backupImage")
|
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
|
||||||
val chapterImages = extractArray(content, "chapterImages")
|
val chapterImages = extractArray(element.html(), "chapterImages")
|
||||||
|
|
||||||
// Add all parsed cdns to set
|
// Add all parsed cdns to set
|
||||||
cdnSet.addAll(cdns)
|
cdnSet.addAll(cdns)
|
||||||
@ -371,10 +369,6 @@ abstract class MangaBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Page(i, document.location(), parsedUrl)
|
Page(i, document.location(), parsedUrl)
|
||||||
}.ifEmpty {
|
|
||||||
document.select("div.container-chapter-reader > img").mapIndexed { i, img ->
|
|
||||||
Page(i, imageUrl = img.absUrl("src"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,8 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 34
|
baseVersionCode = 30
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection UseTomlInstead
|
api(project(":lib:randomua"))
|
||||||
implementation("org.brotli:dec:0.1.2")
|
|
||||||
}
|
}
|
||||||
|
@ -1,71 +1,62 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangahub
|
package eu.kanade.tachiyomi.multisrc.mangahub
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
|
||||||
import androidx.preference.PreferenceScreen
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import keiyoushi.utils.parseAs
|
import kotlinx.serialization.decodeFromString
|
||||||
import keiyoushi.utils.tryParse
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import org.jsoup.nodes.Document
|
||||||
import org.brotli.dec.BrotliInputStream
|
import org.jsoup.nodes.Element
|
||||||
import java.io.ByteArrayOutputStream
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URLEncoder
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
abstract class MangaHub(
|
abstract class MangaHub(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
final override val baseUrl: String,
|
final override val baseUrl: String,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val mangaSource: String,
|
private val mangaSource: String,
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH),
|
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MM-dd-yyyy", Locale.US),
|
||||||
) : HttpSource(), ConfigurableSource {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val baseApiUrl = "https://api.mghcdn.com"
|
private var baseApiUrl = "https://api.mghcdn.com"
|
||||||
private val baseCdnUrl = "https://imgx.mghcdn.com"
|
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
||||||
private val baseThumbCdnUrl = "https://thumb.mghcdn.com"
|
private val regex = Regex("mhub_access=([^;]+)")
|
||||||
private val apiRegex = Regex("mhub_access=([^;]+)")
|
|
||||||
private val spaceRegex = Regex("\\s+")
|
|
||||||
private val apiErrorRegex = Regex("""rate\s*limit|api\s*key""")
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
|
||||||
|
|
||||||
private fun SharedPreferences.getUseGenericTitlePref(): Boolean = getBoolean(
|
|
||||||
PREF_USE_GENERIC_TITLE,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.setRandomUserAgent(
|
||||||
|
userAgentType = UserAgentType.DESKTOP,
|
||||||
|
filterInclude = listOf("chrome"),
|
||||||
|
)
|
||||||
.addInterceptor(::apiAuthInterceptor)
|
.addInterceptor(::apiAuthInterceptor)
|
||||||
.addNetworkInterceptor(::compatEncodingInterceptor)
|
.rateLimit(1)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||||
@ -78,158 +69,60 @@ abstract class MangaHub(
|
|||||||
.add("Sec-Fetch-Site", "same-origin")
|
.add("Sec-Fetch-Site", "same-origin")
|
||||||
.add("Upgrade-Insecure-Requests", "1")
|
.add("Upgrade-Insecure-Requests", "1")
|
||||||
|
|
||||||
private fun postRequestGraphQL(query: String, refreshUrl: String? = null): Request {
|
open val json: Json by injectLazy()
|
||||||
val requestHeaders = headersBuilder()
|
|
||||||
.set("Accept", "application/json")
|
|
||||||
.set("Content-Type", "application/json")
|
|
||||||
.set("Origin", baseUrl)
|
|
||||||
.set("Sec-Fetch-Dest", "empty")
|
|
||||||
.set("Sec-Fetch-Mode", "cors")
|
|
||||||
.set("Sec-Fetch-Site", "cross-site")
|
|
||||||
.removeAll("Upgrade-Insecure-Requests")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val body = buildJsonObject {
|
|
||||||
put("query", query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return POST("$baseApiUrl/graphql", requestHeaders, body.toString().toRequestBody())
|
|
||||||
.newBuilder()
|
|
||||||
.tag(GraphQLTag::class.java, GraphQLTag(refreshUrl))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normally this gets handled properly but in older forks such as TachiyomiJ2K, we have to manually intercept it
|
|
||||||
// as they have an outdated implementation of NetworkHelper.
|
|
||||||
private fun compatEncodingInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
var response = chain.proceed(chain.request())
|
|
||||||
val contentEncoding = response.header("Content-Encoding")
|
|
||||||
|
|
||||||
if (contentEncoding == "gzip") {
|
|
||||||
val parsedBody = response.body.byteStream().let { gzipInputStream ->
|
|
||||||
GZIPInputStream(gzipInputStream).use { inputStream ->
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
outputStream.toByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response = response.createNewWithCompatBody(parsedBody)
|
|
||||||
} else if (contentEncoding == "br") {
|
|
||||||
val parsedBody = response.body.byteStream().let { brotliInputStream ->
|
|
||||||
BrotliInputStream(brotliInputStream).use { inputStream ->
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
outputStream.toByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response = response.createNewWithCompatBody(parsedBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.createNewWithCompatBody(outputStream: ByteArray): Response {
|
|
||||||
return this.newBuilder()
|
|
||||||
.body(outputStream.toResponseBody(this.body.contentType()))
|
|
||||||
.removeHeader("Content-Encoding")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val originalRequest = chain.request()
|
||||||
val tag = request.tag(GraphQLTag::class.java)
|
|
||||||
?: return chain.proceed(request) // We won't intercept non-graphql requests (like image retrieval)
|
|
||||||
|
|
||||||
return try {
|
|
||||||
tryApiRequest(chain, request)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val noCookie = e is MangaHubCookieNotFound
|
|
||||||
val apiError = e is ApiErrorException &&
|
|
||||||
apiErrorRegex.containsMatchIn(e.message ?: "")
|
|
||||||
|
|
||||||
if (noCookie || apiError) {
|
|
||||||
refreshApiKey(tag.refreshUrl)
|
|
||||||
tryApiRequest(chain, request)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryApiRequest(chain: Interceptor.Chain, request: Request): Response {
|
|
||||||
val cookie = client.cookieJar
|
val cookie = client.cookieJar
|
||||||
.loadForRequest(baseUrl.toHttpUrl())
|
.loadForRequest(baseUrl.toHttpUrl())
|
||||||
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
|
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
|
||||||
?: throw MangaHubCookieNotFound()
|
|
||||||
|
|
||||||
val apiRequest = request.newBuilder()
|
val request =
|
||||||
.header("x-mhub-access", cookie.value)
|
if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
|
||||||
.build()
|
originalRequest.newBuilder()
|
||||||
|
.header("x-mhub-access", cookie.value)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
originalRequest
|
||||||
|
}
|
||||||
|
|
||||||
val response = chain.proceed(apiRequest)
|
return chain.proceed(request)
|
||||||
|
|
||||||
val apiResponse = response.peekBody(Long.MAX_VALUE).string()
|
|
||||||
.parseAs<ApiResponseError>()
|
|
||||||
|
|
||||||
if (apiResponse.errors != null) {
|
|
||||||
response.close() // Avoid leaks
|
|
||||||
val errors = apiResponse.errors.joinToString("\n") { it.message }
|
|
||||||
throw ApiErrorException(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MangaHubCookieNotFound : IOException("mhub_access cookie not found")
|
private fun refreshApiKey(chapter: SChapter) {
|
||||||
private class ApiErrorException(errorMessage: String) : IOException(errorMessage)
|
val slug = "$baseUrl${chapter.url}"
|
||||||
|
.toHttpUrlOrNull()
|
||||||
|
?.pathSegments
|
||||||
|
?.get(1)
|
||||||
|
|
||||||
private val lock = ReentrantLock()
|
val url = if (slug != null) {
|
||||||
private var refreshed = 0L
|
"$baseUrl/manga/$slug".toHttpUrl()
|
||||||
|
|
||||||
private fun refreshApiKey(refreshUrl: String? = null) {
|
|
||||||
if (refreshed + 10000 < System.currentTimeMillis() && lock.tryLock()) {
|
|
||||||
val url = when {
|
|
||||||
refreshUrl != null -> refreshUrl
|
|
||||||
else -> "$baseUrl/chapter/martial-peak/chapter-${Random.nextInt(1000, 3000)}"
|
|
||||||
}.toHttpUrl()
|
|
||||||
|
|
||||||
val oldKey = client.cookieJar
|
|
||||||
.loadForRequest(baseUrl.toHttpUrl())
|
|
||||||
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
|
||||||
|
|
||||||
for (i in 1..2) {
|
|
||||||
// Clear key cookie
|
|
||||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
|
||||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We try requesting again with param if the first one fails
|
|
||||||
val query = if (i == 2) "?reloadKey=1" else ""
|
|
||||||
val response = client.newCall(
|
|
||||||
GET(
|
|
||||||
"$url$query",
|
|
||||||
headers.newBuilder()
|
|
||||||
.set("Referer", "$baseUrl/manga/${url.pathSegments[1]}")
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
).execute()
|
|
||||||
val returnedKey =
|
|
||||||
response.headers["set-cookie"]?.let { apiRegex.find(it)?.groupValues?.get(1) }
|
|
||||||
response.close() // Avoid potential resource leaks
|
|
||||||
|
|
||||||
if (returnedKey != oldKey) break // Break out of loop since we got an allegedly valid API key
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
lock.unlock()
|
|
||||||
throw Exception("An error occurred while obtaining a new API key") // Show error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refreshed = System.currentTimeMillis()
|
|
||||||
lock.unlock()
|
|
||||||
} else {
|
} else {
|
||||||
lock.lock() // wait here until lock is released
|
baseUrl.toHttpUrl()
|
||||||
lock.unlock()
|
}
|
||||||
|
|
||||||
|
val oldKey = client.cookieJar
|
||||||
|
.loadForRequest(baseUrl.toHttpUrl())
|
||||||
|
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
||||||
|
|
||||||
|
for (i in 1..2) {
|
||||||
|
// Clear key cookie
|
||||||
|
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
||||||
|
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
||||||
|
|
||||||
|
// We try requesting again with param if the first one fails
|
||||||
|
val query = if (i == 2) "?reloadKey=1" else ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
val response = client.newCall(GET("$url$query", headers)).execute()
|
||||||
|
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
|
||||||
|
response.close() // Avoid potential resource leaks
|
||||||
|
|
||||||
|
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
|
||||||
|
} catch (_: IOException) {
|
||||||
|
throw IOException("An error occurred while obtaining a new API key") // Show error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,36 +133,35 @@ abstract class MangaHub(
|
|||||||
val signature: String,
|
val signature: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun ApiMangaSearchItem.toSignature(): String {
|
private fun Element.toSignature(): String {
|
||||||
val author = this.author
|
val author = this.select("small").text()
|
||||||
val chNum = this.latestChapter
|
val chNum = this.select(".col-sm-6 a:contains(#)").text()
|
||||||
val genres = this.genres
|
val genres = this.select(".genre-label").joinToString { it.text() }
|
||||||
|
|
||||||
return author + chNum + genres
|
return author + chNum + genres
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mangaRequest(page: Int, order: String): Request {
|
|
||||||
return postRequestGraphQL(searchQuery(mangaSource, "", "all", order, page))
|
|
||||||
}
|
|
||||||
|
|
||||||
// popular
|
// popular
|
||||||
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, "POPULAR")
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/popular/page/$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
// often enough there will be nearly identical entries with slightly different
|
// often enough there will be nearly identical entries with slightly different
|
||||||
// titles, URLs, and image names. in order to cut these "duplicates" down,
|
// titles, URLs, and image names. in order to cut these "duplicates" down,
|
||||||
// assign a "signature" based on author name, chapter number, and genres
|
// assign a "signature" based on author name, chapter number, and genres
|
||||||
// if all of those are the same, then it it's the same manga
|
// if all of those are the same, then it it's the same manga
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val mangaList = response.parseAs<ApiSearchResponse>()
|
val doc = response.asJsoup()
|
||||||
|
|
||||||
val mangas = mangaList.data.search.rows.map {
|
val mangas = doc.select(popularMangaSelector())
|
||||||
SMangaDTO(
|
.map {
|
||||||
"$baseUrl/manga/${it.slug}",
|
SMangaDTO(
|
||||||
it.title,
|
it.select("h4 a").attr("abs:href"),
|
||||||
"$baseThumbCdnUrl/${it.image}",
|
it.select("h4 a").text(),
|
||||||
it.toSignature(),
|
it.select("img").attr("abs:src"),
|
||||||
)
|
it.toSignature(),
|
||||||
}
|
)
|
||||||
|
}
|
||||||
.distinctBy { it.signature }
|
.distinctBy { it.signature }
|
||||||
.map {
|
.map {
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
@ -278,171 +170,221 @@ abstract class MangaHub(
|
|||||||
thumbnail_url = it.thumbnailUrl
|
thumbnail_url = it.thumbnailUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return MangasPage(mangas, doc.select(popularMangaNextPageSelector()).isNotEmpty())
|
||||||
// Entries have a max of 30 per request
|
|
||||||
return MangasPage(mangas, mangaList.data.search.rows.count() == 30)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".col-sm-6:not(:has(a:contains(Yaoi)))"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "ul.pager li.next > a"
|
||||||
|
|
||||||
// latest
|
// latest
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return mangaRequest(page, "LATEST")
|
return GET("$baseUrl/updates/page/$page", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
return popularMangaParse(response)
|
return popularMangaParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
// search
|
// search
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
var order = "POPULAR"
|
val url = "$baseUrl/search/page/$page".toHttpUrl().newBuilder()
|
||||||
var genres = "all"
|
url.addQueryParameter("q", query)
|
||||||
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is OrderBy -> {
|
is OrderBy -> {
|
||||||
order = filter.values[filter.state].key
|
val order = filter.values[filter.state]
|
||||||
|
url.addQueryParameter("order", order.key)
|
||||||
}
|
}
|
||||||
is GenreList -> {
|
is GenreList -> {
|
||||||
genres = filter.included.joinToString(",").takeIf { it.isNotBlank() } ?: "all"
|
val genre = filter.values[filter.state]
|
||||||
|
url.addQueryParameter("genre", genre.key)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
return postRequestGraphQL(searchQuery(mangaSource, query, genres, order, page))
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
return popularMangaParse(response)
|
return popularMangaParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
// manga details
|
// manga details
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
return postRequestGraphQL(
|
val manga = SManga.create()
|
||||||
mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")),
|
manga.title = document.select(".breadcrumb .active span").text()
|
||||||
refreshUrl = "$baseUrl${manga.url}",
|
manga.author = document.select("div:has(h1) span:contains(Author) + span").first()?.text()
|
||||||
)
|
manga.artist = document.select("div:has(h1) span:contains(Artist) + span").first()?.text()
|
||||||
}
|
manga.genre = document.select(".row p a").joinToString { it.text() }
|
||||||
|
manga.description = document.select(".tab-content p").first()?.text()
|
||||||
|
manga.thumbnail_url = document.select("img.img-responsive").first()
|
||||||
|
?.attr("src")
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
document.select("div:has(h1) span:contains(Status) + span").first()?.text()?.also { statusText ->
|
||||||
val rawManga = response.parseAs<ApiMangaDetailsResponse>()
|
when {
|
||||||
|
statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
|
||||||
return SManga.create().apply {
|
statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
|
||||||
title = rawManga.data.manga.title!!
|
else -> manga.status = SManga.UNKNOWN
|
||||||
author = rawManga.data.manga.author
|
|
||||||
artist = rawManga.data.manga.artist
|
|
||||||
genre = rawManga.data.manga.genres
|
|
||||||
thumbnail_url = "$baseThumbCdnUrl/${rawManga.data.manga.image}"
|
|
||||||
status = when (rawManga.data.manga.status) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
description = buildString {
|
// add alternative name to manga description
|
||||||
rawManga.data.manga.description?.let(::append)
|
document.select("h1 small").firstOrNull()?.ownText()?.let { alternativeName ->
|
||||||
|
if (alternativeName.isNotBlank()) {
|
||||||
// Add alternative title
|
manga.description = manga.description.orEmpty().let {
|
||||||
val altTitle = rawManga.data.manga.alternativeTitle
|
if (it.isBlank()) {
|
||||||
if (!altTitle.isNullOrBlank()) {
|
"Alternative Name: $alternativeName"
|
||||||
if (isNotBlank()) append("\n\n")
|
} else {
|
||||||
append("Alternative Name: $altTitle")
|
"$it\n\nAlternative Name: $alternativeName"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
|
// chapters
|
||||||
|
|
||||||
// Chapters
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
return postRequestGraphQL(
|
|
||||||
mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")),
|
|
||||||
refreshUrl = "$baseUrl${manga.url}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapterList = response.parseAs<ApiMangaDetailsResponse>()
|
val document = response.asJsoup()
|
||||||
val useGenericTitle = preferences.getUseGenericTitlePref()
|
val head = document.head()
|
||||||
|
return document.select(chapterListSelector()).map { chapterFromElement(it, head) }
|
||||||
return chapterList.data.manga.chapters!!.map {
|
|
||||||
SChapter.create().apply {
|
|
||||||
val numberString = "${if (it.number % 1 == 0f) it.number.toInt() else it.number}"
|
|
||||||
|
|
||||||
name = if (!useGenericTitle) {
|
|
||||||
generateChapterName(it.title.trim().replace(spaceRegex, " "), numberString)
|
|
||||||
} else {
|
|
||||||
generateGenericChapterName(numberString)
|
|
||||||
}
|
|
||||||
|
|
||||||
url = "/${chapterList.data.manga.slug}/chapter-${it.number}"
|
|
||||||
chapter_number = it.number
|
|
||||||
date_upload = dateFormat.tryParse(it.date)
|
|
||||||
}
|
|
||||||
}.reversed() // The response is sorted in ASC format so we need to reverse it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateChapterName(title: String, number: String): String {
|
override fun chapterListSelector() = ".tab-content ul li"
|
||||||
return if (title.contains(number)) {
|
|
||||||
title
|
private fun chapterFromElement(element: Element, head: Element): SChapter {
|
||||||
} else if (title.isNotBlank()) {
|
val chapter = SChapter.create()
|
||||||
"Chapter $number - $title"
|
val potentialLinks = element.select("a[href*='$baseUrl/chapter/']:not([rel*=nofollow]):not([rel*=noreferrer])")
|
||||||
} else {
|
var visibleLink = ""
|
||||||
generateGenericChapterName(number)
|
potentialLinks.forEach { a ->
|
||||||
|
val className = a.className()
|
||||||
|
val styles = head.select("style").html()
|
||||||
|
if (!styles.contains(".$className { display:none; }")) {
|
||||||
|
visibleLink = a.attr("href")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
chapter.setUrlWithoutDomain(visibleLink)
|
||||||
|
chapter.name = chapter.url.trimEnd('/').substringAfterLast('/').replace('-', ' ')
|
||||||
|
chapter.date_upload = element.select("small.UovLc").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
return chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateGenericChapterName(number: String): String {
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
return "Chapter $number"
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/chapter${chapter.url}"
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
val now = Calendar.getInstance().apply {
|
||||||
// Pages
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
set(Calendar.MINUTE, 0)
|
||||||
val chapterUrl = chapter.url.split("/")
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
return postRequestGraphQL(
|
}
|
||||||
pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()),
|
var parsedDate = 0L
|
||||||
refreshUrl = "$baseUrl/chapter${chapter.url}",
|
when {
|
||||||
)
|
"just now" in date || "less than an hour" in date -> {
|
||||||
}
|
parsedDate = now.timeInMillis
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val chapterObject = response.parseAs<ApiChapterPagesResponse>()
|
|
||||||
val pages = chapterObject.data.chapter.pages.parseAs<ApiChapterPages>()
|
|
||||||
|
|
||||||
// We'll update the cookie here to match the browser's "recently" opened chapter.
|
|
||||||
// This mimics how the browser works and gives us more chance to receive a valid API key upon refresh
|
|
||||||
val now = Calendar.getInstance().time.time
|
|
||||||
val baseHttpUrl = baseUrl.toHttpUrl()
|
|
||||||
val recently = buildJsonObject {
|
|
||||||
putJsonObject((now).toString()) {
|
|
||||||
put("mangaID", chapterObject.data.chapter.mangaID)
|
|
||||||
put("number", chapterObject.data.chapter.chapterNumber)
|
|
||||||
}
|
}
|
||||||
}.toString()
|
// parses: "1 hour ago" and "2 hours ago"
|
||||||
|
"hour" in date -> {
|
||||||
|
val hours = date.replaceAfter(" ", "").trim().toInt()
|
||||||
|
parsedDate = now.apply { add(Calendar.HOUR, -hours) }.timeInMillis
|
||||||
|
}
|
||||||
|
// parses: "Yesterday" and "2 days ago"
|
||||||
|
"day" in date -> {
|
||||||
|
val days = date.replace("days ago", "").trim().toIntOrNull() ?: 1
|
||||||
|
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -days) }.timeInMillis
|
||||||
|
}
|
||||||
|
// parses: "2 weeks ago"
|
||||||
|
"weeks" in date -> {
|
||||||
|
val weeks = date.replace("weeks ago", "").trim().toInt()
|
||||||
|
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -weeks) }.timeInMillis
|
||||||
|
}
|
||||||
|
// parses: "12-20-2019" and defaults everything that wasn't taken into account to 0
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
parsedDate = dateFormat.parse(date)?.time ?: 0L
|
||||||
|
} catch (e: ParseException) { /*nothing to do, parsedDate is initialized with 0L*/ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedDate
|
||||||
|
}
|
||||||
|
|
||||||
val recentlyCookie = Cookie.Builder()
|
// pages
|
||||||
.domain(baseHttpUrl.host)
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
.name("recently")
|
val body = buildJsonObject {
|
||||||
.value(URLEncoder.encode(recently, "utf-8"))
|
put("query", PAGES_QUERY)
|
||||||
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
|
put(
|
||||||
|
"variables",
|
||||||
|
buildJsonObject {
|
||||||
|
val chapterUrl = chapter.url.split("/")
|
||||||
|
|
||||||
|
put("mangaSource", mangaSource)
|
||||||
|
put("slug", chapterUrl[2])
|
||||||
|
put("number", chapterUrl[3].substringAfter("-").toFloat())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toString()
|
||||||
|
.toRequestBody()
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Accept", "application/json")
|
||||||
|
.set("Content-Type", "application/json")
|
||||||
|
.set("Origin", baseUrl)
|
||||||
|
.set("Sec-Fetch-Dest", "empty")
|
||||||
|
.set("Sec-Fetch-Mode", "cors")
|
||||||
|
.set("Sec-Fetch-Site", "cross-site")
|
||||||
|
.removeAll("Upgrade-Insecure-Requests")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Add/update the cookie
|
return POST("$baseApiUrl/graphql", newHeaders, body)
|
||||||
client.cookieJar.saveFromResponse(baseHttpUrl, listOf(recentlyCookie))
|
}
|
||||||
|
|
||||||
// We'll log our action to the site to further increase the chance of valid API key
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||||
val ipRequest = client.newCall(GET("https://api.ipify.org?format=json")).execute()
|
super.fetchPageList(chapter)
|
||||||
val ip = ipRequest.parseAs<PublicIPResponse>().ip
|
.doOnError { refreshApiKey(chapter) }
|
||||||
|
.retry(1)
|
||||||
|
|
||||||
client.newCall(GET("$baseUrl/action/logHistory2/${chapterObject.data.chapter.manga.slug}/${chapterObject.data.chapter.chapterNumber}?browserID=$ip")).execute().close()
|
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
|
||||||
ipRequest.close()
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
|
||||||
|
|
||||||
return pages.images.mapIndexed { i, page ->
|
if (chapterObject.data?.chapter == null) {
|
||||||
Page(i, "", "$baseCdnUrl/${pages.page}$page")
|
if (chapterObject.errors != null) {
|
||||||
|
val errors = chapterObject.errors.joinToString("\n") { it.message }
|
||||||
|
throw Exception(errors)
|
||||||
|
}
|
||||||
|
throw Exception("Unknown error while processing pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
val pages = json.decodeFromString<ApiChapterPages>(chapterObject.data.chapter.pages)
|
||||||
|
|
||||||
|
return pages.i.mapIndexed { i, page ->
|
||||||
|
Page(i, "", "$baseCdnUrl/${pages.p}$page")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,14 +401,10 @@ abstract class MangaHub(
|
|||||||
return GET(page.url, newHeaders)
|
return GET(page.url, newHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
private class Genre(title: String, val key: String) : Filter.CheckBox(title) {
|
private class Genre(title: String, val key: String) : Filter.TriState(title) {
|
||||||
fun getGenreKey(): String {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@ -479,14 +417,11 @@ abstract class MangaHub(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
|
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
|
||||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) {
|
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Genres", genres, 0)
|
||||||
val included: List<String>
|
|
||||||
get() = state.filter { it.state }.map { it.getGenreKey() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
GenreList(genres),
|
|
||||||
OrderBy(orderBy),
|
OrderBy(orderBy),
|
||||||
|
GenreList(genres),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val orderBy = arrayOf(
|
private val orderBy = arrayOf(
|
||||||
@ -497,119 +432,70 @@ abstract class MangaHub(
|
|||||||
Order("Completed", "COMPLETED"),
|
Order("Completed", "COMPLETED"),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val genres = listOf(
|
private val genres = arrayOf(
|
||||||
|
Genre("All Genres", "all"),
|
||||||
|
Genre("[no chapters]", "no-chapters"),
|
||||||
|
Genre("4-Koma", "4-koma"),
|
||||||
Genre("Action", "action"),
|
Genre("Action", "action"),
|
||||||
Genre("Adventure", "adventure"),
|
Genre("Adventure", "adventure"),
|
||||||
|
Genre("Award Winning", "award-winning"),
|
||||||
Genre("Comedy", "comedy"),
|
Genre("Comedy", "comedy"),
|
||||||
Genre("Adult", "adult"),
|
|
||||||
Genre("Drama", "drama"),
|
|
||||||
Genre("Historical", "historical"),
|
|
||||||
Genre("Martial Arts", "martial-arts"),
|
|
||||||
Genre("Romance", "romance"),
|
|
||||||
Genre("Ecchi", "ecchi"),
|
|
||||||
Genre("Supernatural", "supernatural"),
|
|
||||||
Genre("Webtoons", "webtoons"),
|
|
||||||
Genre("Manhwa", "manhwa"),
|
|
||||||
Genre("Fantasy", "fantasy"),
|
|
||||||
Genre("Harem", "harem"),
|
|
||||||
Genre("Shounen", "shounen"),
|
|
||||||
Genre("Manhua", "manhua"),
|
|
||||||
Genre("Mature", "mature"),
|
|
||||||
Genre("Seinen", "seinen"),
|
|
||||||
Genre("Sports", "sports"),
|
|
||||||
Genre("School Life", "school-life"),
|
|
||||||
Genre("Smut", "smut"),
|
|
||||||
Genre("Mystery", "mystery"),
|
|
||||||
Genre("Psychological", "psychological"),
|
|
||||||
Genre("Shounen ai", "shounen-ai"),
|
|
||||||
Genre("Slice of life", "slice-of-life"),
|
|
||||||
Genre("Shoujo ai", "shoujo-ai"),
|
|
||||||
Genre("Cooking", "cooking"),
|
Genre("Cooking", "cooking"),
|
||||||
Genre("Horror", "horror"),
|
Genre("Crime", "crime"),
|
||||||
Genre("Tragedy", "tragedy"),
|
Genre("Demons", "demons"),
|
||||||
Genre("Doujinshi", "doujinshi"),
|
Genre("Doujinshi", "doujinshi"),
|
||||||
Genre("Sci-Fi", "sci-fi"),
|
Genre("Drama", "drama"),
|
||||||
Genre("Yuri", "yuri"),
|
Genre("Ecchi", "ecchi"),
|
||||||
Genre("Yaoi", "yaoi"),
|
Genre("Fantasy", "fantasy"),
|
||||||
Genre("Shoujo", "shoujo"),
|
Genre("Food", "food"),
|
||||||
|
Genre("Game", "game"),
|
||||||
Genre("Gender bender", "gender-bender"),
|
Genre("Gender bender", "gender-bender"),
|
||||||
|
Genre("Harem", "harem"),
|
||||||
|
Genre("Historical", "historical"),
|
||||||
|
Genre("Horror", "horror"),
|
||||||
|
Genre("Isekai", "isekai"),
|
||||||
Genre("Josei", "josei"),
|
Genre("Josei", "josei"),
|
||||||
|
Genre("Kids", "kids"),
|
||||||
|
Genre("Magic", "magic"),
|
||||||
|
Genre("Magical Girls", "magical-girls"),
|
||||||
|
Genre("Manhua", "manhua"),
|
||||||
|
Genre("Manhwa", "manhwa"),
|
||||||
|
Genre("Martial arts", "martial-arts"),
|
||||||
|
Genre("Mature", "mature"),
|
||||||
Genre("Mecha", "mecha"),
|
Genre("Mecha", "mecha"),
|
||||||
Genre("Medical", "medical"),
|
Genre("Medical", "medical"),
|
||||||
Genre("Magic", "magic"),
|
|
||||||
Genre("4-Koma", "4-koma"),
|
|
||||||
Genre("Music", "music"),
|
|
||||||
Genre("Webtoon", "webtoon"),
|
|
||||||
Genre("Isekai", "isekai"),
|
|
||||||
Genre("Game", "game"),
|
|
||||||
Genre("Award Winning", "award-winning"),
|
|
||||||
Genre("Oneshot", "oneshot"),
|
|
||||||
Genre("Demons", "demons"),
|
|
||||||
Genre("Military", "military"),
|
Genre("Military", "military"),
|
||||||
|
Genre("Music", "music"),
|
||||||
|
Genre("Mystery", "mystery"),
|
||||||
|
Genre("One shot", "one-shot"),
|
||||||
|
Genre("Oneshot", "oneshot"),
|
||||||
|
Genre("Parody", "parody"),
|
||||||
Genre("Police", "police"),
|
Genre("Police", "police"),
|
||||||
|
Genre("Psychological", "psychological"),
|
||||||
|
Genre("Romance", "romance"),
|
||||||
|
Genre("School life", "school-life"),
|
||||||
|
Genre("Sci fi", "sci-fi"),
|
||||||
|
Genre("Seinen", "seinen"),
|
||||||
|
Genre("Shotacon", "shotacon"),
|
||||||
|
Genre("Shoujo", "shoujo"),
|
||||||
|
Genre("Shoujo ai", "shoujo-ai"),
|
||||||
|
Genre("Shoujoai", "shoujoai"),
|
||||||
|
Genre("Shounen", "shounen"),
|
||||||
|
Genre("Shounen ai", "shounen-ai"),
|
||||||
|
Genre("Shounenai", "shounenai"),
|
||||||
|
Genre("Slice of life", "slice-of-life"),
|
||||||
|
Genre("Smut", "smut"),
|
||||||
|
Genre("Space", "space"),
|
||||||
|
Genre("Sports", "sports"),
|
||||||
Genre("Super Power", "super-power"),
|
Genre("Super Power", "super-power"),
|
||||||
Genre("Food", "food"),
|
|
||||||
Genre("Kids", "kids"),
|
|
||||||
Genre("Magical Girls", "magical-girls"),
|
|
||||||
Genre("Wuxia", "wuxia"),
|
|
||||||
Genre("Superhero", "superhero"),
|
Genre("Superhero", "superhero"),
|
||||||
|
Genre("Supernatural", "supernatural"),
|
||||||
Genre("Thriller", "thriller"),
|
Genre("Thriller", "thriller"),
|
||||||
Genre("Crime", "crime"),
|
Genre("Tragedy", "tragedy"),
|
||||||
Genre("Philosophical", "philosophical"),
|
Genre("Vampire", "vampire"),
|
||||||
Genre("Adaptation", "adaptation"),
|
Genre("Webtoon", "webtoon"),
|
||||||
Genre("Full Color", "full-color"),
|
Genre("Webtoons", "webtoons"),
|
||||||
Genre("Crossdressing", "crossdressing"),
|
Genre("Wuxia", "wuxia"),
|
||||||
Genre("Reincarnation", "reincarnation"),
|
Genre("Yuri", "yuri"),
|
||||||
Genre("Manga", "manga"),
|
)
|
||||||
Genre("Cartoon", "cartoon"),
|
|
||||||
Genre("Survival", "survival"),
|
|
||||||
Genre("Comic", "comic"),
|
|
||||||
Genre("English", "english"),
|
|
||||||
Genre("Harlequin", "harlequin"),
|
|
||||||
Genre("Time Travel", "time-travel"),
|
|
||||||
Genre("Traditional Games", "traditional-games"),
|
|
||||||
Genre("Reverse Harem", "reverse-harem"),
|
|
||||||
Genre("Animals", "animals"),
|
|
||||||
Genre("Aliens", "aliens"),
|
|
||||||
Genre("Loli", "loli"),
|
|
||||||
Genre("Video Games", "video-games"),
|
|
||||||
Genre("Monsters", "monsters"),
|
|
||||||
Genre("Office Workers", "office-workers"),
|
|
||||||
Genre("System", "system"),
|
|
||||||
Genre("Villainess", "villainess"),
|
|
||||||
Genre("Zombies", "zombies"),
|
|
||||||
Genre("Vampires", "vampires"),
|
|
||||||
Genre("Violence", "violence"),
|
|
||||||
Genre("Monster Girls", "monster-girls"),
|
|
||||||
Genre("Anthology", "anthology"),
|
|
||||||
Genre("Ghosts", "ghosts"),
|
|
||||||
Genre("Delinquents", "delinquents"),
|
|
||||||
Genre("Post-Apocalyptic", "post-apocalyptic"),
|
|
||||||
Genre("Xianxia", "xianxia"),
|
|
||||||
Genre("Xuanhuan", "xuanhuan"),
|
|
||||||
Genre("R-18", "r-18"),
|
|
||||||
Genre("Cultivation", "cultivation"),
|
|
||||||
Genre("Rebirth", "rebirth"),
|
|
||||||
Genre("Gore", "gore"),
|
|
||||||
Genre("Russian", "russian"),
|
|
||||||
Genre("Samurai", "samurai"),
|
|
||||||
Genre("Ninja", "ninja"),
|
|
||||||
Genre("Revenge", "revenge"),
|
|
||||||
Genre("Cheat Systems", "cheat-systems"),
|
|
||||||
Genre("Dungeons", "dungeons"),
|
|
||||||
Genre("Overpowered", "overpowered"),
|
|
||||||
).sortedBy { it.toString() }
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = PREF_USE_GENERIC_TITLE
|
|
||||||
title = "Use generic title"
|
|
||||||
summary = "Use generic chapter title (\"Chapter 'x'\") instead of the given one.\nNote: May require manga entry to be refreshed."
|
|
||||||
setDefaultValue(false)
|
|
||||||
}.let(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREF_USE_GENERIC_TITLE = "pref_use_generic_title"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
)
|
|
@ -1,70 +1,42 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangahub
|
package eu.kanade.tachiyomi.multisrc.mangahub
|
||||||
|
|
||||||
class GraphQLTag(
|
import kotlinx.serialization.Serializable
|
||||||
val refreshUrl: String? = null,
|
|
||||||
|
private fun buildQuery(queryAction: () -> String) = queryAction().replace("%", "$")
|
||||||
|
|
||||||
|
val PAGES_QUERY = buildQuery {
|
||||||
|
"""
|
||||||
|
query(%mangaSource: MangaSource, %slug: String!, %number: Float!) {
|
||||||
|
chapter(x: %mangaSource, slug: %slug, number: %number) {
|
||||||
|
pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiErrorMessages(
|
||||||
|
val message: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val searchQuery = { mangaSource: String, query: String, genre: String, order: String, page: Int ->
|
@Serializable
|
||||||
"""
|
data class ApiChapterPagesResponse(
|
||||||
{
|
val data: ApiChapterData?,
|
||||||
search(x: $mangaSource, q: "$query", genre: "$genre", mod: $order, offset: ${(page - 1) * 30}) {
|
val errors: List<ApiErrorMessages>?,
|
||||||
rows {
|
)
|
||||||
title,
|
|
||||||
author,
|
|
||||||
slug,
|
|
||||||
image,
|
|
||||||
genres,
|
|
||||||
latestChapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaDetailsQuery = { mangaSource: String, slug: String ->
|
@Serializable
|
||||||
"""
|
data class ApiChapterData(
|
||||||
{
|
val chapter: ApiChapter?,
|
||||||
manga(x: $mangaSource, slug: "$slug") {
|
)
|
||||||
title,
|
|
||||||
slug,
|
|
||||||
status,
|
|
||||||
image,
|
|
||||||
author,
|
|
||||||
artist,
|
|
||||||
genres,
|
|
||||||
description,
|
|
||||||
alternativeTitle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaChapterListQuery = { mangaSource: String, slug: String ->
|
@Serializable
|
||||||
"""
|
data class ApiChapter(
|
||||||
{
|
val pages: String,
|
||||||
manga(x: $mangaSource, slug: "$slug") {
|
)
|
||||||
slug,
|
|
||||||
chapters {
|
|
||||||
number,
|
|
||||||
title,
|
|
||||||
date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
val pagesQuery = { mangaSource: String, slug: String, number: Float ->
|
@Serializable
|
||||||
"""
|
data class ApiChapterPages(
|
||||||
{
|
val p: String,
|
||||||
chapter(x: $mangaSource, slug: "$slug", number: $number) {
|
val i: List<String>,
|
||||||
pages,
|
)
|
||||||
mangaID,
|
|
||||||
number,
|
|
||||||
manga {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 3
|
||||||
|
@ -14,12 +14,6 @@ class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val response = chain.proceed(request)
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
val contentType = response.header("content-type")
|
|
||||||
if (contentType != null && contentType.startsWith("image/", ignoreCase = true)) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore requests that already have completed the JS challenge
|
// ignore requests that already have completed the JS challenge
|
||||||
if (response.headers["vary"] != null) return response
|
if (response.headers["vary"] != null) return response
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'BaoBua'
|
|
||||||
extClass = '.BaoBua'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 50
|
extVersionCode = 49
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +279,7 @@ open class BatoTo(
|
|||||||
manga.title = infoElement.select("h3").text().removeEntities()
|
manga.title = infoElement.select("h3").text().removeEntities()
|
||||||
manga.thumbnail_url = document.select("div.attr-cover img")
|
manga.thumbnail_url = document.select("div.attr-cover img")
|
||||||
.attr("abs:src")
|
.attr("abs:src")
|
||||||
manga.setUrlWithoutDomain(infoElement.select("h3 a").attr("abs:href"))
|
manga.url = infoElement.select("h3 a").attr("abs:href")
|
||||||
return MangasPage(listOf(manga), false)
|
return MangasPage(listOf(manga), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +405,7 @@ open class BatoTo(
|
|||||||
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
|
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
|
||||||
.select("channel > item").map { item ->
|
.select("channel > item").map { item ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
setUrlWithoutDomain(item.selectFirst("guid")!!.text())
|
url = item.selectFirst("guid")!!.text()
|
||||||
name = item.selectFirst("title")!!.text()
|
name = item.selectFirst("title")!!.text()
|
||||||
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
|
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Buon Dua'
|
extName = 'Buon Dua'
|
||||||
extClass = '.BuonDua'
|
extClass = '.BuonDua'
|
||||||
extVersionCode = 4
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:randomua"))
|
|
||||||
}
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.buondua
|
package eu.kanade.tachiyomi.extension.all.buondua
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -11,15 +8,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import keiyoushi.utils.tryParse
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class BuonDua() : ParsedHttpSource() {
|
class BuonDua() : ParsedHttpSource() {
|
||||||
override val baseUrl = "https://buondua.com"
|
override val baseUrl = "https://buondua.com"
|
||||||
@ -27,13 +20,6 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
override val name = "Buon Dua"
|
override val name = "Buon Dua"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
|
|
||||||
.setRandomUserAgent(UserAgentType.MOBILE)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
@ -57,10 +43,10 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/hot?start=${20 * (page - 1)}")
|
return GET("$baseUrl/hot?start=${20 * (page - 1)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = latestUpdatesSelector()
|
override fun popularMangaSelector() = latestUpdatesSelector()
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||||
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
@ -71,7 +57,6 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
else -> popularMangaRequest(page)
|
else -> popularMangaRequest(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = latestUpdatesSelector()
|
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
@ -87,27 +72,34 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() = throw UnsupportedOperationException()
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
val chapter = SChapter.create()
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
chapter.setUrlWithoutDomain(element.select(".is-current").first()!!.attr("abs:href"))
|
||||||
val doc = response.asJsoup()
|
chapter.chapter_number = 0F
|
||||||
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
|
chapter.name = element.select(".article-header").text()
|
||||||
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
|
chapter.date_upload = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US).parse(element.select(".article-info > small").text())?.time ?: 0L
|
||||||
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1
|
return chapter
|
||||||
val basePageUrl = response.request.url
|
|
||||||
return (maxPage downTo 1).map { page ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain("$basePageUrl?page=$page")
|
|
||||||
name = "Page $page"
|
|
||||||
date_upload = dateUpload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "html"
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select(".article-fulltext img")
|
val numpages = document.selectFirst(".pagination-list")!!.select(".pagination-link")
|
||||||
.mapIndexed { i, imgEl -> Page(i, imageUrl = imgEl.absUrl("src")) }
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
numpages.forEachIndexed { index, page ->
|
||||||
|
val doc = when (index) {
|
||||||
|
0 -> document
|
||||||
|
else -> client.newCall(GET(page.attr("abs:href"))).execute().asJsoup()
|
||||||
|
}
|
||||||
|
doc.select(".article-fulltext img").forEach {
|
||||||
|
val itUrl = it.attr("abs:src")
|
||||||
|
pages.add(Page(pages.size, "", itUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
@ -122,8 +114,4 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
class TagFilter : Filter.Text("Tag ID")
|
class TagFilter : Filter.Text("Tag ID")
|
||||||
|
|
||||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATE_FORMAT = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
ignored_groups_title=Ignored Groups
|
ignored_groups_title=Ignored Groups
|
||||||
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
||||||
ignored_tags_title=Ignored Tags
|
|
||||||
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
|
|
||||||
show_alternative_titles_title=Show Alternative Titles
|
show_alternative_titles_title=Show Alternative Titles
|
||||||
show_alternative_titles_on=Adds alternative titles to the description
|
show_alternative_titles_on=Adds alternative titles to the description
|
||||||
show_alternative_titles_off=Does not show alternative titles to the description
|
show_alternative_titles_off=Does not show alternative titles to the description
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
extClass = '.ComickFactory'
|
extClass = '.ComickFactory'
|
||||||
extVersionCode = 56
|
extVersionCode = 55
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,12 +79,6 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = IGNORED_TAGS_PREF
|
|
||||||
title = intl["ignored_tags_title"]
|
|
||||||
summary = intl["ignored_tags_summary"]
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = SHOW_ALTERNATIVE_TITLES_PREF
|
key = SHOW_ALTERNATIVE_TITLES_PREF
|
||||||
title = intl["show_alternative_titles_title"]
|
title = intl["show_alternative_titles_title"]
|
||||||
@ -190,14 +184,6 @@ abstract class Comick(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
private val SharedPreferences.ignoredTags: String
|
|
||||||
get() = getString(IGNORED_TAGS_PREF, "")
|
|
||||||
?.split("\n")
|
|
||||||
?.map(String::trim)
|
|
||||||
?.filter(String::isNotEmpty)
|
|
||||||
.orEmpty()
|
|
||||||
.joinToString(",")
|
|
||||||
|
|
||||||
private val SharedPreferences.showAlternativeTitles: Boolean
|
private val SharedPreferences.showAlternativeTitles: Boolean
|
||||||
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
||||||
|
|
||||||
@ -257,13 +243,8 @@ abstract class Comick(
|
|||||||
|
|
||||||
/** Popular Manga **/
|
/** Popular Manga **/
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return searchMangaRequest(
|
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
|
||||||
page = page,
|
return GET(url, headers)
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("follow"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
@ -276,13 +257,8 @@ abstract class Comick(
|
|||||||
|
|
||||||
/** Latest Manga **/
|
/** Latest Manga **/
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return searchMangaRequest(
|
val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$LIMIT&page=$page&tachiyomi=true"
|
||||||
page = page,
|
return GET(url, headers)
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("uploaded"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||||
@ -340,7 +316,7 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
|
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
|
||||||
tags.split(",").filter(String::isNotEmpty).forEach {
|
tags.split(",").forEach {
|
||||||
builder.addQueryParameter(
|
builder.addQueryParameter(
|
||||||
parameterName,
|
parameterName,
|
||||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
||||||
@ -436,7 +412,6 @@ abstract class Comick(
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
|
|
||||||
addQueryParameter("tachiyomi", "true")
|
addQueryParameter("tachiyomi", "true")
|
||||||
addQueryParameter("limit", "$LIMIT")
|
addQueryParameter("limit", "$LIMIT")
|
||||||
addQueryParameter("page", "$page")
|
addQueryParameter("page", "$page")
|
||||||
@ -612,7 +587,6 @@ abstract class Comick(
|
|||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||||
private const val IGNORED_TAGS_PREF = "IgnoredTags"
|
|
||||||
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
|
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
|
||||||
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
|
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
|
||||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||||
|
@ -9,7 +9,7 @@ fun getFilters(): FilterList {
|
|||||||
GenreFilter("Genre", getGenresList),
|
GenreFilter("Genre", getGenresList),
|
||||||
DemographicFilter("Demographic", getDemographicList),
|
DemographicFilter("Demographic", getDemographicList),
|
||||||
TypeFilter("Type", getTypeList),
|
TypeFilter("Type", getTypeList),
|
||||||
SortFilter(),
|
SortFilter("Sort", getSortsList),
|
||||||
StatusFilter("Status", getStatusList),
|
StatusFilter("Status", getStatusList),
|
||||||
ContentRatingFilter("Content Rating", getContentRatingList),
|
ContentRatingFilter("Content Rating", getContentRatingList),
|
||||||
CompletedFilter("Completely Scanlated?"),
|
CompletedFilter("Completely Scanlated?"),
|
||||||
@ -50,8 +50,8 @@ internal class FromYearFilter(name: String) : TextFilter(name)
|
|||||||
|
|
||||||
internal class ToYearFilter(name: String) : TextFilter(name)
|
internal class ToYearFilter(name: String) : TextFilter(name)
|
||||||
|
|
||||||
internal class SortFilter(defaultValue: String? = null, state: Int = 0) :
|
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
|
||||||
SelectFilter("Sort", getSortsList, state, defaultValue)
|
SelectFilter(name, sortList, state)
|
||||||
|
|
||||||
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
||||||
SelectFilter(name, statusList, state)
|
SelectFilter(name, statusList, state)
|
||||||
@ -66,8 +66,8 @@ internal open class TextFilter(name: String) : Filter.Text(name)
|
|||||||
|
|
||||||
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
|
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
|
||||||
|
|
||||||
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0, defaultValue: String? = null) :
|
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) {
|
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||||
fun getValue() = vals[state].second
|
fun getValue() = vals[state].second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'DeviantArt'
|
extName = 'DeviantArt'
|
||||||
extClass = '.DeviantArt'
|
extClass = '.DeviantArt'
|
||||||
extVersionCode = 8
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,11 +134,12 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList.also(::orderChapterList).toList()
|
return chapterList.toList().also(::indexChapterList)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseToChapterList(document: Document): List<SChapter> {
|
private fun parseToChapterList(document: Document): List<SChapter> {
|
||||||
return document.select("item").map {
|
val items = document.select("item")
|
||||||
|
return items.map {
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||||
name = it.selectFirst("title")!!.text()
|
name = it.selectFirst("title")!!.text()
|
||||||
@ -148,15 +149,17 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun orderChapterList(chapterList: MutableList<SChapter>) {
|
private fun indexChapterList(chapterList: List<SChapter>) {
|
||||||
// In Mihon's updates tab, chapters are ordered by source instead
|
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
||||||
// of chapter number, so to avoid updates being shown in reverse,
|
// primitively index the list by checking the first and last dates
|
||||||
// disregard source order and order chronologically instead
|
if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||||
if (chapterList.first().date_upload < chapterList.last().date_upload) {
|
chapterList.forEachIndexed { i, chapter ->
|
||||||
chapterList.reverse()
|
chapter.chapter_number = chapterList.size - i.toFloat()
|
||||||
}
|
}
|
||||||
chapterList.forEachIndexed { i, chapter ->
|
} else {
|
||||||
chapter.chapter_number = chapterList.size - i.toFloat()
|
chapterList.forEachIndexed { i, chapter ->
|
||||||
|
chapter.chapter_number = i.toFloat() + 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.EternalMangasFactory'
|
extClass = '.EternalMangasFactory'
|
||||||
themePkg = 'mangaesp'
|
themePkg = 'mangaesp'
|
||||||
baseUrl = 'https://eternalmangas.com'
|
baseUrl = 'https://eternalmangas.com'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,24 +2,16 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
|
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
|
||||||
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
|
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import rx.Observable
|
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -34,58 +26,16 @@ open class EternalMangas(
|
|||||||
) {
|
) {
|
||||||
override val useApiSearch = true
|
override val useApiSearch = true
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
return super.fetchSearchManga(page, "", createSortFilter("views", false))
|
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||||
}
|
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga(seriesPath) } ?: emptyList()
|
||||||
|
return MangasPage(mangas, false)
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
|
||||||
return super.fetchSearchManga(page, "", createSortFilter("updated_at", false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
||||||
return this.filter { it.language == internalLang }.toMutableList()
|
return this.filter { it.language == internalLang }.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return GET("$baseUrl/comics", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dataUrl = "https://raw.githubusercontent.com/bapeey/extensions-tools/refs/heads/main/keiyoushi/eternalmangas/values.txt"
|
|
||||||
|
|
||||||
override fun searchMangaParse(
|
|
||||||
response: Response,
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: FilterList,
|
|
||||||
): MangasPage {
|
|
||||||
val (apiComicsUrl, jsonHeaders, useApi, scriptSelector, comicsRegex) = client.newCall(GET(dataUrl)).execute().body.string().split("\n")
|
|
||||||
val apiSearch = useApi == "1"
|
|
||||||
comicsList = if (apiSearch) {
|
|
||||||
val headersJson = json.parseToJsonElement(jsonHeaders).jsonObject
|
|
||||||
val apiHeaders = headersBuilder()
|
|
||||||
headersJson.forEach { (key, jsonElement) ->
|
|
||||||
var value = jsonElement.jsonPrimitive.contentOrNull.orEmpty()
|
|
||||||
if (value.startsWith("1-")) {
|
|
||||||
val match = value.substringAfter("-").toRegex().find(response.body.string())
|
|
||||||
value = match?.groupValues?.get(1).orEmpty()
|
|
||||||
} else {
|
|
||||||
value = value.substringAfter("-")
|
|
||||||
}
|
|
||||||
apiHeaders.add(key, value)
|
|
||||||
}
|
|
||||||
val apiResponse = client.newCall(GET(apiComicsUrl, apiHeaders.build())).execute()
|
|
||||||
json.decodeFromString<List<SeriesDto>>(apiResponse.body.string()).toMutableList()
|
|
||||||
} else {
|
|
||||||
val script = response.asJsoup().select(scriptSelector).joinToString { it.data() }
|
|
||||||
val jsonString = comicsRegex.toRegex().find(script)?.groupValues?.get(1)
|
|
||||||
?: throw Exception(intl["comics_list_error"])
|
|
||||||
val unescapedJson = jsonString.unescape()
|
|
||||||
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseComicsList(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||||
val body = jsRedirect(response)
|
val body = jsRedirect(response)
|
||||||
|
|
||||||
@ -142,7 +92,7 @@ open class EternalMangas(
|
|||||||
private fun jsRedirect(response: Response): String {
|
private fun jsRedirect(response: Response): String {
|
||||||
var body = response.body.string()
|
var body = response.body.string()
|
||||||
val document = Jsoup.parse(body)
|
val document = Jsoup.parse(body)
|
||||||
document.selectFirst("body > form[method=post], body > div[hidden] > form[method=post]")?.let {
|
document.selectFirst("body > form[method=post]")?.let {
|
||||||
val action = it.attr("action")
|
val action = it.attr("action")
|
||||||
val inputs = it.select("input")
|
val inputs = it.select("input")
|
||||||
|
|
||||||
@ -156,13 +106,8 @@ open class EternalMangas(
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSortFilter(value: String, ascending: Boolean = false): FilterList {
|
@Serializable
|
||||||
val sortProperties = getSortProperties()
|
class LatestUpdatesDto(
|
||||||
val index = sortProperties.indexOfFirst { it.value == value }.takeIf { it >= 0 } ?: 0
|
val updates: Map<String, List<List<SeriesDto>>>,
|
||||||
return FilterList(
|
)
|
||||||
SortByFilter("", sortProperties).apply {
|
|
||||||
state = Filter.Sort.Selection(index, ascending)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
@ -1,12 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'GlobalComix'
|
|
||||||
extClass = '.GlobalComixFactory'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:i18n"))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 13 KiB |
@ -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") }
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
@ -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")
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
@ -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
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 39
|
extVersionCode = 38
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -52,7 +52,7 @@ class Hitomi(
|
|||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::imageUrlInterceptor)
|
.addInterceptor(::updateImageUrlInterceptor)
|
||||||
.apply {
|
.apply {
|
||||||
interceptors().add(0, ::streamResetRetry)
|
interceptors().add(0, ::streamResetRetry)
|
||||||
}
|
}
|
||||||
@ -491,20 +491,18 @@ class Hitomi(
|
|||||||
}.awaitAll().filterNotNull()
|
}.awaitAll().filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Gallery.toSManga() = SManga.create().apply {
|
private suspend fun Gallery.toSManga() = SManga.create().apply {
|
||||||
title = this@toSManga.title
|
title = this@toSManga.title
|
||||||
url = galleryurl
|
url = galleryurl
|
||||||
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
|
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
|
||||||
artist = artists?.joinToString { it.formatted }
|
artist = artists?.joinToString { it.formatted }
|
||||||
genre = tags?.joinToString { it.formatted }
|
genre = tags?.joinToString { it.formatted }
|
||||||
thumbnail_url = files.first().let {
|
thumbnail_url = files.first().let {
|
||||||
HttpUrl.Builder().apply {
|
val hash = it.hash
|
||||||
scheme("https")
|
val imageId = imageIdFromHash(hash)
|
||||||
host(IMAGE_LOOPBACK_HOST)
|
val subDomain = 'a' + subdomainOffset(imageId)
|
||||||
addQueryParameter(IMAGE_THUMBNAIL, "true")
|
|
||||||
addQueryParameter(IMAGE_GIF, it.isGif.toString())
|
"https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
|
||||||
fragment(it.hash)
|
|
||||||
}.toString()
|
|
||||||
}
|
}
|
||||||
description = buildString {
|
description = buildString {
|
||||||
japaneseTitle?.let {
|
japaneseTitle?.let {
|
||||||
@ -573,13 +571,11 @@ class Hitomi(
|
|||||||
.substringBefore(".")
|
.substringBefore(".")
|
||||||
|
|
||||||
return gallery.files.mapIndexed { idx, img ->
|
return gallery.files.mapIndexed { idx, img ->
|
||||||
// actual logic in imageUrlInterceptor
|
// actual logic in updateImageUrlInterceptor
|
||||||
val imageUrl = HttpUrl.Builder().apply {
|
val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
|
||||||
scheme("https")
|
.fragment(img.hash)
|
||||||
host(IMAGE_LOOPBACK_HOST)
|
.build()
|
||||||
addQueryParameter(IMAGE_GIF, img.isGif.toString())
|
.toString()
|
||||||
fragment(img.hash)
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
Page(
|
Page(
|
||||||
idx,
|
idx,
|
||||||
@ -681,38 +677,18 @@ class Hitomi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun imageUrlInterceptor(chain: Interceptor.Chain): Response {
|
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
if (request.url.host != IMAGE_LOOPBACK_HOST) {
|
if (request.url.host != "127.0.0.1") {
|
||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hash = request.url.fragment!!
|
val hash = request.url.fragment!!
|
||||||
val isThumbnail = request.url.queryParameter(IMAGE_THUMBNAIL) == "true"
|
val commonId = runBlocking { commonImageId() }
|
||||||
val isGif = request.url.queryParameter(IMAGE_GIF) == "true"
|
|
||||||
|
|
||||||
val type = if (isGif) {
|
|
||||||
"webp"
|
|
||||||
} else {
|
|
||||||
"avif"
|
|
||||||
}
|
|
||||||
val imageId = imageIdFromHash(hash)
|
val imageId = imageIdFromHash(hash)
|
||||||
val subDomainOffset = runBlocking { subdomainOffset(imageId) }
|
val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
|
||||||
|
|
||||||
val imageUrl = if (isThumbnail) {
|
val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
|
||||||
val subDomain = "${'a' + subDomainOffset}tn"
|
|
||||||
|
|
||||||
"https://$subDomain.$cdnDomain/${type}bigtn/${thumbPathFromHash(hash)}/$hash.$type"
|
|
||||||
} else {
|
|
||||||
val commonId = runBlocking { commonImageId() }
|
|
||||||
val subDomain = if (isGif) {
|
|
||||||
"w${subDomainOffset + 1}"
|
|
||||||
} else {
|
|
||||||
"a${subDomainOffset + 1}"
|
|
||||||
}
|
|
||||||
|
|
||||||
"https://$subDomain.$cdnDomain/$commonId$imageId/$hash.$type"
|
|
||||||
}
|
|
||||||
|
|
||||||
val newRequest = request.newBuilder()
|
val newRequest = request.newBuilder()
|
||||||
.url(imageUrl)
|
.url(imageUrl)
|
||||||
@ -729,7 +705,3 @@ class Hitomi(
|
|||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
const val IMAGE_LOOPBACK_HOST = "127.0.0.1"
|
|
||||||
const val IMAGE_THUMBNAIL = "is_thumbnail"
|
|
||||||
const val IMAGE_GIF = "is_gif"
|
|
||||||
|
@ -22,10 +22,7 @@ class Gallery(
|
|||||||
@Serializable
|
@Serializable
|
||||||
class ImageFile(
|
class ImageFile(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
private val name: String,
|
)
|
||||||
) {
|
|
||||||
val isGif get() = name.endsWith(".gif")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Tag(
|
class Tag(
|
||||||
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.KdtScans'
|
extClass = '.KdtScans'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://kdtscans.com'
|
baseUrl = 'https://kdtscans.com'
|
||||||
overrideVersionCode = 2
|
overrideVersionCode = 0
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,14 @@ import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class KdtScans : Madara(
|
class KdtScans : Madara(
|
||||||
"KDT Scans",
|
"KDT Scans",
|
||||||
"https://kdtscans.com",
|
"https://kdtscans.com",
|
||||||
"all",
|
"all",
|
||||||
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
||||||
) {
|
) {
|
||||||
override val useNewChapterEndpoint = true
|
override val useNewChapterEndpoint = true
|
||||||
override val fetchGenres = false
|
override val fetchGenres = false
|
||||||
@ -19,8 +22,6 @@ class KdtScans : Madara(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.c-tabs-item__content:not(:contains([LN]))"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga {
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
return super.searchMangaFromElement(element).apply {
|
return super.searchMangaFromElement(element).apply {
|
||||||
title = title.cleanupTitle()
|
title = title.cleanupTitle()
|
||||||
@ -36,7 +37,5 @@ class KdtScans : Madara(
|
|||||||
private fun String.cleanupTitle() = replace(titleCleanupRegex, "").trim()
|
private fun String.cleanupTitle() = replace(titleCleanupRegex, "").trim()
|
||||||
|
|
||||||
private val titleCleanupRegex =
|
private val titleCleanupRegex =
|
||||||
Regex("""^\[(ESPAÑOL|English|HD|VIP)\]\s+(–\s+)?""", RegexOption.IGNORE_CASE)
|
Regex("""^\[(ESPAÑOL|English)\]\s+(–\s+)?""", RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
override fun chapterListSelector() = "li.wp-manga-chapter:not(:has(.required-login))"
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Kiutaku'
|
extName = 'Kiutaku'
|
||||||
extClass = '.Kiutaku'
|
extClass = '.Kiutaku'
|
||||||
extVersionCode = 3
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,12 +78,7 @@ class Kiutaku : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder()
|
return GET("$baseUrl/?search=$query&start=${getPage(page)}", headers)
|
||||||
.addQueryParameter("search", query)
|
|
||||||
.addQueryParameter("start", getPage(page).toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'SchaleNetwork'
|
extName = 'SchaleNetwork'
|
||||||
extClass = '.KoharuFactory'
|
extClass = '.KoharuFactory'
|
||||||
extVersionCode = 14
|
extVersionCode = 13
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.authorization
|
|||||||
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token
|
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -122,9 +120,7 @@ class TurnstileInterceptor(
|
|||||||
try {
|
try {
|
||||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||||
val authHeaders = authHeaders(authHeader)
|
val authHeaders = authHeaders(authHeader)
|
||||||
val response = runBlocking(Dispatchers.IO) {
|
val response = noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
|
||||||
noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
|
|
||||||
}
|
|
||||||
response.use {
|
response.use {
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
with(response) {
|
with(response) {
|
||||||
@ -180,9 +176,7 @@ class TurnstileInterceptor(
|
|||||||
try {
|
try {
|
||||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||||
val authHeaders = authHeaders("Bearer $token")
|
val authHeaders = authHeaders("Bearer $token")
|
||||||
val response = runBlocking(Dispatchers.IO) {
|
val response = noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
|
||||||
noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
|
|
||||||
}
|
|
||||||
response.use {
|
response.use {
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
return true
|
return true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Komga'
|
extName = 'Komga'
|
||||||
extClass = '.KomgaFactory'
|
extClass = '.KomgaFactory'
|
||||||
extVersionCode = 63
|
extVersionCode = 61
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -134,12 +134,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
else -> "series"
|
else -> "series"
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = "$baseUrl/api/v1".toHttpUrl().newBuilder()
|
val url = "$baseUrl/api/v1/$type?search=$query&page=${page - 1}&deleted=false".toHttpUrl().newBuilder()
|
||||||
.addPathSegments(type)
|
|
||||||
.addQueryParameter("search", query)
|
|
||||||
.addQueryParameter("page", (page - 1).toString())
|
|
||||||
.addQueryParameter("deleted", "false")
|
|
||||||
|
|
||||||
val filterList = filters.ifEmpty { getFilterList() }
|
val filterList = filters.ifEmpty { getFilterList() }
|
||||||
val defaultLibraries = defaultLibraries
|
val defaultLibraries = defaultLibraries
|
||||||
|
|
||||||
@ -188,7 +183,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
|
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url)
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
return if (response.isFromReadList()) {
|
return if (response.isFromReadList()) {
|
||||||
@ -259,7 +254,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
.sortedByDescending { it.chapter_number }
|
.sortedByDescending { it.chapter_number }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages", headers)
|
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages")
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val pages = response.parseAs<List<PageDto>>()
|
val pages = response.parseAs<List<PageDto>>()
|
||||||
@ -472,17 +467,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
libraries = client.newCall(GET("$baseUrl/api/v1/libraries", headers)).await().parseAs()
|
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).await().parseAs()
|
||||||
collections = client
|
collections = client
|
||||||
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true", headers))
|
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<PageWrapperDto<CollectionDto>>()
|
.parseAs<PageWrapperDto<CollectionDto>>()
|
||||||
.content
|
.content
|
||||||
genres = client.newCall(GET("$baseUrl/api/v1/genres", headers)).await().parseAs()
|
genres = client.newCall(GET("$baseUrl/api/v1/genres")).await().parseAs()
|
||||||
tags = client.newCall(GET("$baseUrl/api/v1/tags", headers)).await().parseAs()
|
tags = client.newCall(GET("$baseUrl/api/v1/tags")).await().parseAs()
|
||||||
publishers = client.newCall(GET("$baseUrl/api/v1/publishers", headers)).await().parseAs()
|
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).await().parseAs()
|
||||||
authors = client
|
authors = client
|
||||||
.newCall(GET("$baseUrl/api/v1/authors", headers))
|
.newCall(GET("$baseUrl/api/v1/authors"))
|
||||||
.await()
|
.await()
|
||||||
.parseAs<List<AuthorDto>>()
|
.parseAs<List<AuthorDto>>()
|
||||||
.groupBy { it.role }
|
.groupBy { it.role }
|
||||||
|
@ -38,7 +38,7 @@ class SeriesDto(
|
|||||||
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
genre = (metadata.genres + metadata.tags + booksMetadata.tags).sorted().distinct().joinToString(", ")
|
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
|
||||||
description = metadata.summary.ifBlank { booksMetadata.summary }
|
description = metadata.summary.ifBlank { booksMetadata.summary }
|
||||||
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
|
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
|
||||||
author = map["writer"]?.distinct()?.joinToString()
|
author = map["writer"]?.distinct()?.joinToString()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MangaFire'
|
extName = 'MangaFire'
|
||||||
extClass = '.MangaFireFactory'
|
extClass = '.MangaFireFactory'
|
||||||
extVersionCode = 12
|
extVersionCode = 11
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +45,6 @@ class MangaFire(
|
|||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
|
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="https"
|
<data android:scheme="https"
|
||||||
android:host="mangago.fit"
|
android:host="mangahosted.org"
|
||||||
android:pathPattern="/.*/..*" />
|
android:pathPattern="/.*/..*" />
|
||||||
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Manga Hosted'
|
extName = 'Manga Hosted'
|
||||||
extClass = '.MangaHostedFactory'
|
extClass = '.MangaHostedFactory'
|
||||||
extVersionCode = 3
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
|
|||||||
|
|
||||||
override val name: String = "Manga Hosted${langOption.nameSuffix}"
|
override val name: String = "Manga Hosted${langOption.nameSuffix}"
|
||||||
|
|
||||||
override val baseUrl: String = "https://mangago.fit/${langOption.infix}"
|
override val baseUrl: String = "https://mangahosted.org"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
|
|||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
if (query.startsWith(SEARCH_PREFIX)) {
|
if (query.startsWith(SEARCH_PREFIX)) {
|
||||||
val url = "$baseUrl/${query.substringAfter(SEARCH_PREFIX)}"
|
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
|
||||||
return client.newCall(GET(url, headers))
|
return client.newCall(GET(url, headers))
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
|
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
|
||||||
@ -184,7 +184,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
|
|||||||
title = dto.title
|
title = dto.title
|
||||||
thumbnail_url = dto.thumbnailUrl
|
thumbnail_url = dto.thumbnailUrl
|
||||||
status = dto.status
|
status = dto.status
|
||||||
url = "/${dto.slug}"
|
url = "/${langOption.infix}/${dto.slug}"
|
||||||
genre = dto.genres
|
genre = dto.genres
|
||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SEARCH_PREFIX = "slug:"
|
const val SEARCH_PREFIX = "slug:"
|
||||||
val baseApiUrl = "https://api.mangago.fit"
|
val baseApiUrl = "https://api.novelfull.us"
|
||||||
val apiUrl = "$baseApiUrl/api"
|
val apiUrl = "$baseApiUrl/api"
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ val languages = listOf(
|
|||||||
LanguageOption("id", "manga-indo", "id"),
|
LanguageOption("id", "manga-indo", "id"),
|
||||||
LanguageOption("it", "manga-italia", "manga-it"),
|
LanguageOption("it", "manga-italia", "manga-it"),
|
||||||
LanguageOption("ja", "mangaraw", "raw"),
|
LanguageOption("ja", "mangaraw", "raw"),
|
||||||
LanguageOption("pt-BR", "manga-br"),
|
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
|
||||||
LanguageOption("ru", "manga-ru", "mangaru"),
|
LanguageOption("ru", "manga-ru", "mangaru"),
|
||||||
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
|
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
|
||||||
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
|
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MangaPark'
|
extName = 'MangaPark'
|
||||||
extClass = '.MangaParkFactory'
|
extClass = '.MangaParkFactory'
|
||||||
extVersionCode = 22
|
extVersionCode = 21
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,19 +17,20 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import keiyoushi.utils.firstInstanceOrNull
|
|
||||||
import keiyoushi.utils.getPreferences
|
import keiyoushi.utils.getPreferences
|
||||||
import keiyoushi.utils.parseAs
|
|
||||||
import keiyoushi.utils.toJsonString
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@ -53,15 +54,13 @@ class MangaPark(
|
|||||||
|
|
||||||
private val apiUrl = "$baseUrl/apo/"
|
private val apiUrl = "$baseUrl/apo/"
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder().apply {
|
private val json: Json by injectLazy()
|
||||||
if (preference.getBoolean(ENABLE_NSFW, true)) {
|
|
||||||
addInterceptor(::siteSettingsInterceptor)
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
|
.addInterceptor(::siteSettingsInterceptor)
|
||||||
}
|
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
|
||||||
rateLimitHost(apiUrl.toHttpUrl(), 1)
|
.rateLimitHost(apiUrl.toHttpUrl(), 1)
|
||||||
// intentionally after rate limit interceptor so thumbnails are not rate limited
|
.build()
|
||||||
addInterceptor(::thumbnailDomainInterceptor)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.set("Referer", "$baseUrl/")
|
||||||
@ -97,10 +96,8 @@ class MangaPark(
|
|||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val result = response.parseAs<SearchResponse>()
|
val result = response.parseAs<SearchResponse>()
|
||||||
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
|
|
||||||
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
|
|
||||||
|
|
||||||
val entries = result.data.searchComics.items.map { it.data.toSManga(shortenTitle, pageAsCover) }
|
val entries = result.data.searchComics.items.map { it.data.toSManga() }
|
||||||
val hasNextPage = entries.size == size
|
val hasNextPage = entries.size == size
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
return MangasPage(entries, hasNextPage)
|
||||||
@ -167,10 +164,8 @@ class MangaPark(
|
|||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val result = response.parseAs<DetailsResponse>()
|
val result = response.parseAs<DetailsResponse>()
|
||||||
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
|
|
||||||
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
|
|
||||||
|
|
||||||
return result.data.comic.data.toSManga(shortenTitle, pageAsCover)
|
return result.data.comic.data.toSManga()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
|
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
|
||||||
@ -225,7 +220,7 @@ class MangaPark(
|
|||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
setOnPreferenceChangeListener { _, _ ->
|
||||||
Toast.makeText(screen.context, "Restart the app to apply changes", Toast.LENGTH_LONG).show()
|
Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
@ -236,34 +231,16 @@ class MangaPark(
|
|||||||
summary = "Refresh chapter list to apply changes"
|
summary = "Refresh chapter list to apply changes"
|
||||||
setDefaultValue(false)
|
setDefaultValue(false)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = ENABLE_NSFW
|
|
||||||
title = "Enable NSFW content"
|
|
||||||
summary = "Clear Cookies & Restart the app to apply changes."
|
|
||||||
setDefaultValue(true)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = SHORTEN_TITLE_PREF
|
|
||||||
title = "Remove extra information from title"
|
|
||||||
summary = "Clear database to apply changes\n\n" +
|
|
||||||
"Note: doesn't not work for entries in library"
|
|
||||||
setDefaultValue(false)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = UNCENSORED_COVER_PREF
|
|
||||||
title = "Attempt to use Uncensored Cover for Hentai"
|
|
||||||
summary = "Uses first or last chapter page as cover"
|
|
||||||
entries = arrayOf("Off", "First Chapter", "Last Chapter")
|
|
||||||
entryValues = arrayOf("off", "first", "last")
|
|
||||||
setDefaultValue("off")
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
use { body.string() }.let(json::decodeFromString)
|
||||||
|
|
||||||
|
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
|
||||||
|
filterIsInstance<T>().firstOrNull()
|
||||||
|
|
||||||
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
private inline fun <reified T : Any> T.toJsonRequestBody() =
|
||||||
toJsonString().toRequestBody(JSON_MEDIA_TYPE)
|
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
private val cookiesNotSet = AtomicBoolean(true)
|
private val cookiesNotSet = AtomicBoolean(true)
|
||||||
private val latch = CountDownLatch(1)
|
private val latch = CountDownLatch(1)
|
||||||
@ -294,25 +271,6 @@ class MangaPark(
|
|||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun thumbnailDomainInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val url = request.url
|
|
||||||
|
|
||||||
return if (url.host == THUMBNAIL_LOOPBACK_HOST) {
|
|
||||||
val newUrl = url.newBuilder()
|
|
||||||
.host(domain)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val newRequest = request.newBuilder()
|
|
||||||
.url(newUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
chain.proceed(newRequest)
|
|
||||||
} else {
|
|
||||||
chain.proceed(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
@ -340,11 +298,6 @@ class MangaPark(
|
|||||||
"mpark.to",
|
"mpark.to",
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val ENABLE_NSFW = "pref_nsfw"
|
|
||||||
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
|
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
|
||||||
private const val SHORTEN_TITLE_PREF = "pref_shorten_title"
|
|
||||||
private const val UNCENSORED_COVER_PREF = "pref_uncensored_cover"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val THUMBNAIL_LOOPBACK_HOST = "127.0.0.1"
|
|
||||||
|
@ -38,68 +38,33 @@ class MangaParkComic(
|
|||||||
private val originalStatus: String? = null,
|
private val originalStatus: String? = null,
|
||||||
private val uploadStatus: String? = null,
|
private val uploadStatus: String? = null,
|
||||||
private val summary: String? = null,
|
private val summary: String? = null,
|
||||||
private val extraInfo: String? = null,
|
|
||||||
@SerialName("urlCoverOri") private val cover: String? = null,
|
@SerialName("urlCoverOri") private val cover: String? = null,
|
||||||
private val urlPath: String,
|
private val urlPath: String,
|
||||||
@SerialName("max_chapterNode") private val latestChapter: Data<ImageFiles>? = null,
|
|
||||||
@SerialName("first_chapterNode") private val firstChapter: Data<ImageFiles>? = null,
|
|
||||||
) {
|
) {
|
||||||
fun toSManga(shortenTitle: Boolean, pageAsCover: String) = SManga.create().apply {
|
fun toSManga() = SManga.create().apply {
|
||||||
url = "$urlPath#$id"
|
url = "$urlPath#$id"
|
||||||
title = if (shortenTitle) {
|
title = name
|
||||||
var shortName = name
|
thumbnail_url = cover
|
||||||
while (shortenTitleRegex.containsMatchIn(shortName)) {
|
|
||||||
shortName = shortName.replace(shortenTitleRegex, "").trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
shortName
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
thumbnail_url = run {
|
|
||||||
val coverUrl = cover?.let {
|
|
||||||
when {
|
|
||||||
it.startsWith("http") -> it
|
|
||||||
it.startsWith("/") -> "https://$THUMBNAIL_LOOPBACK_HOST$it"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageAsCover != "off" && useLatestPageAsCover(genres)) {
|
|
||||||
if (pageAsCover == "first") {
|
|
||||||
firstChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
|
|
||||||
} else {
|
|
||||||
latestChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
coverUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
author = authors?.joinToString()
|
author = authors?.joinToString()
|
||||||
artist = artists?.joinToString()
|
artist = artists?.joinToString()
|
||||||
description = buildString {
|
description = buildString {
|
||||||
if (shortenTitle) {
|
val desc = summary?.let { Jsoup.parse(it).text() }
|
||||||
append(name)
|
val names = altNames?.takeUnless { it.isEmpty() }
|
||||||
append("\n\n")
|
?.joinToString("\n") { "• ${it.trim()}" }
|
||||||
|
|
||||||
|
if (desc.isNullOrEmpty()) {
|
||||||
|
if (!names.isNullOrEmpty()) {
|
||||||
|
append("Alternative Names:\n", names)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(desc)
|
||||||
|
if (!names.isNullOrEmpty()) {
|
||||||
|
append("\n\nAlternative Names:\n", names)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
summary?.also {
|
}
|
||||||
append(Jsoup.parse(it).wholeText().trim())
|
|
||||||
append("\n\n")
|
|
||||||
}
|
|
||||||
extraInfo?.takeUnless(String::isBlank)?.also {
|
|
||||||
append("Extra Info:\n")
|
|
||||||
append(Jsoup.parse(it).wholeText().trim())
|
|
||||||
append("\n\n")
|
|
||||||
}
|
|
||||||
altNames?.takeUnless(List<String>::isEmpty)
|
|
||||||
?.joinToString(
|
|
||||||
prefix = "Alternative Names:\n",
|
|
||||||
separator = "\n",
|
|
||||||
) { "• ${it.trim()}" }
|
|
||||||
?.also(::append)
|
|
||||||
}.trim()
|
|
||||||
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
|
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
|
||||||
status = when (originalStatus ?: uploadStatus) {
|
status = when (originalStatus) {
|
||||||
"ongoing" -> SManga.ONGOING
|
"ongoing" -> SManga.ONGOING
|
||||||
"completed" -> {
|
"completed" -> {
|
||||||
if (uploadStatus == "ongoing") {
|
if (uploadStatus == "ongoing") {
|
||||||
@ -131,14 +96,6 @@ class MangaParkComic(
|
|||||||
}
|
}
|
||||||
return result.toString()
|
return result.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun useLatestPageAsCover(genres: List<String>?): Boolean {
|
|
||||||
return genres.orEmpty().let {
|
|
||||||
it.contains("hentai") && !it.contains("webtoon")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val shortenTitleRegex = Regex("""^(\[[^]]+\])|^(\([^)]+\))|^(\{[^}]+\})|(\[[^]]+\])${'$'}|(\([^)]+\))${'$'}|(\{[^}]+\})${'$'}""")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,23 +25,8 @@ val SEARCH_QUERY = buildQuery {
|
|||||||
originalStatus
|
originalStatus
|
||||||
uploadStatus
|
uploadStatus
|
||||||
summary
|
summary
|
||||||
extraInfo
|
|
||||||
urlCoverOri
|
urlCoverOri
|
||||||
urlPath
|
urlPath
|
||||||
max_chapterNode {
|
|
||||||
data {
|
|
||||||
imageFile {
|
|
||||||
urlList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
first_chapterNode {
|
|
||||||
data {
|
|
||||||
imageFile {
|
|
||||||
urlList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,23 +52,8 @@ val DETAILS_QUERY = buildQuery {
|
|||||||
originalStatus
|
originalStatus
|
||||||
uploadStatus
|
uploadStatus
|
||||||
summary
|
summary
|
||||||
extraInfo
|
|
||||||
urlCoverOri
|
urlCoverOri
|
||||||
urlPath
|
urlPath
|
||||||
max_chapterNode {
|
|
||||||
data {
|
|
||||||
imageFile {
|
|
||||||
urlList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
first_chapterNode {
|
|
||||||
data {
|
|
||||||
imageFile {
|
|
||||||
urlList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.Manhwa18CcFactory'
|
extClass = '.Manhwa18CcFactory'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manhwa18.cc'
|
baseUrl = 'https://manhwa18.cc'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -27,7 +28,7 @@ class Manhwa18CcEN : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "en") {
|
|||||||
|
|
||||||
class Manhwa18CcKO : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "ko") {
|
class Manhwa18CcKO : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "ko") {
|
||||||
override fun popularMangaSelector() = "div.manga-item:has(h3 a[title$='Raw'])"
|
override fun popularMangaSelector() = "div.manga-item:has(h3 a[title$='Raw'])"
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page", headers)
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page")
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Manhwa18Cc(
|
abstract class Manhwa18Cc(
|
||||||
@ -44,9 +45,9 @@ abstract class Manhwa18Cc(
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "ul.pagination li.next a"
|
override fun popularMangaNextPageSelector() = "ul.pagination li.next a"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending", headers)
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending")
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page", headers)
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page")
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
@ -59,12 +60,7 @@ abstract class Manhwa18Cc(
|
|||||||
// "No results found" message. So this fix redirect to popular page.
|
// "No results found" message. So this fix redirect to popular page.
|
||||||
if (query.isBlank()) return popularMangaRequest(page)
|
if (query.isBlank()) return popularMangaRequest(page)
|
||||||
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
return GET("$baseUrl/search?q=$query&page=$page")
|
||||||
.addQueryParameter("q", query)
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val mangaSubString = "webtoon"
|
override val mangaSubString = "webtoon"
|
||||||
@ -76,4 +72,16 @@ abstract class Manhwa18Cc(
|
|||||||
override fun chapterDateSelector() = "span.chapter-time"
|
override fun chapterDateSelector() = "span.chapter-time"
|
||||||
|
|
||||||
override val pageListParseSelector = "div.read-content img"
|
override val pageListParseSelector = "div.read-content img"
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select(pageListParseSelector).mapIndexed { index, element ->
|
||||||
|
Page(
|
||||||
|
index,
|
||||||
|
document.location(),
|
||||||
|
element?.let {
|
||||||
|
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'MissKon'
|
|
||||||
extClass = '.MissKon'
|
|
||||||
extVersionCode = 2
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 17 KiB |