Compare commits
171 Commits
e91f02af02
...
988d1b04af
Author | SHA1 | Date |
---|---|---|
Vetle Ledaal | 988d1b04af | |
Fioren | 924e064e42 | |
are-are-are | 3766a24892 | |
Chopper | 554872b754 | |
dngonz | 1b3368110f | |
Altometer | 23d3d70961 | |
Chopper | 15f07bd2bd | |
Hellkaros | 87c6e30551 | |
Chopper | 7ae3695832 | |
dngonz | 03accf8717 | |
are-are-are | c9af13410f | |
Chopper | 5687f616b2 | |
Tim Schneeberger | 4ceef9ca9b | |
Chopper | cf29bdafd3 | |
Hellkaros | e7549fd4c1 | |
Hellkaros | 9f62ebd9a3 | |
dngonz | e12546007e | |
dngonz | 80aa0347fe | |
dngonz | 39cd7eb03f | |
dngonz | 8dfcf0cea7 | |
Chopper | 49efe333db | |
Hellkaros | d3d31ec815 | |
Aurel | e86d2a850b | |
Fathul Hidayat | 8d8d46aad1 | |
Jefferson Abreu Martinez | 34cd0bafcd | |
Hellkaros | b088ba606b | |
Hellkaros | f5708a63fc | |
Hellkaros | 32cea00999 | |
Aurel | 079efdfea2 | |
rebel onion | 5ac0b9b5f6 | |
Tim Schneeberger | d470490087 | |
AwkwardPeak7 | a837998ad8 | |
mrtear | 82ac20e710 | |
Chopper | 4f0481f71d | |
Chopper | 699740accb | |
Smol Ame | f1428f8938 | |
Smol Ame | 007b3f32bc | |
bapeey | bdadddf27c | |
Chopper | 8d91fc6551 | |
Jefferson Abreu Martinez | 3bea2c543e | |
dngonz | 91b1c3e65e | |
Chopper | 038e9dcdcb | |
Chopper | 580b2b1b16 | |
dngonz | 690c553b6c | |
dngonz | e1c5cc473e | |
dngonz | c9b85253ed | |
Dr1ks | 677d9d17c0 | |
pjy612 | b24bc5b9ca | |
bapeey | bcfcc110aa | |
bapeey | 7fc0445474 | |
AwkwardPeak7 | 075bcde304 | |
Dr1ks | de1f46782f | |
Dr1ks | 57d4f6f855 | |
dngonz | 60dcde32b1 | |
Cuong-Tran | 4bc138aa39 | |
Dr1ks | 6f90a79c96 | |
Dr1ks | 7f168d0cd1 | |
dngonz | 5a4c6a39bb | |
suhaien | 8e86f6b723 | |
Chopper | d7f724243c | |
Chopper | 73013e9d46 | |
Chopper | 509cc58346 | |
Chopper | aa06c5631f | |
Dr1ks | d328389cc7 | |
dngonz | 05aebf390a | |
dngonz | ad73175a1f | |
Chopper | c88b8b28aa | |
iloverabbit | f64996d23c | |
dngonz | f8bafa79ee | |
Vetle Ledaal | ab7b4fa09e | |
dngonz | efc3642c17 | |
Romain | 4a1a040b23 | |
dngonz | b7409c8efd | |
Chopper | d9822c7ef5 | |
morallkat | ffd43e2873 | |
usagisang | 5d402cc101 | |
dngonz | 3e2f37043b | |
Chopper | f72e042cce | |
suhaien | 06dd598b45 | |
AwkwardPeak7 | 6b8b650004 | |
Chopper | bb2e8d2cde | |
Chopper | b4314f5f0b | |
Dr1ks | 0a3cc9a886 | |
Chopper | b1f2459ddc | |
duongtra | df2a548325 | |
Vetle Ledaal | 77ae53ba1e | |
Vetle Ledaal | 299d0f1ad8 | |
Fioren | 0c56c453f5 | |
dngonz | 280ae02b9b | |
dngonz | 2821b0dce8 | |
Vetle Ledaal | 5143199e08 | |
Vetle Ledaal | 61c8073679 | |
Chopper | 0615bf338a | |
Chopper | 4f8715d432 | |
Chopper | 943b7992c8 | |
dngonz | ecfa117f5d | |
kana-shii | 32aa5f3808 | |
kana-shii | 0002103804 | |
Dr1ks | 18b1977691 | |
dngonz | 64b447a4ac | |
Chopper | 32f9674e70 | |
DokterKaj | 6c67e88e5b | |
mrtear | bd0f21c65e | |
mrtear | a83f7d4f97 | |
Chopper | ae7fd918dd | |
Creepler13 | a838bad72c | |
Vetle Ledaal | dcb623cdb3 | |
Chopper | 059e3267af | |
mrtear | 31fff66ff8 | |
haruki-takeshi | 5a3e302137 | |
Michał Marszałek | 696f4f2bc9 | |
Vetle Ledaal | 44ad6961d3 | |
dngonz | 39ae8f5e73 | |
dngonz | 77debf098e | |
zhongfly | c9a27a8b51 | |
Creepler13 | 4647af4f9c | |
Chopper | 68ccc79b5b | |
Vetle Ledaal | 4e1beae2b7 | |
FunnyTiming | 70efa61570 | |
DokterKaj | 065ff132a0 | |
AwkwardPeak7 | c3c87863b2 | |
AwkwardPeak7 | 919a6490bb | |
Chopper | cf8b7f3f31 | |
Chopper | b0250b98f7 | |
Chopper | 5723bb392d | |
Chopper | 7573bca926 | |
AlphaBoom | f2208ff245 | |
renovate[bot] | a90166842c | |
Eshlender | d9661d2617 | |
zhongfly | 7813eb9117 | |
Chopper | 820806815b | |
Chopper | a3a2580c84 | |
Creepler13 | 01a1ae5d1c | |
Chopper | c97115f9ba | |
DokterKaj | 9ead615784 | |
Lev | 0c399f549e | |
lord-ne | e78ddcb9ed | |
duongtra | 9ad46416f5 | |
Chaos Pjeles | 512716f5aa | |
AwkwardPeak7 | af92d1591d | |
alberlandohc | caf440aaa2 | |
Creepler13 | 34abdec28c | |
bapeey | d89120eb49 | |
bapeey | a383626ae6 | |
bapeey | 6a476ee786 | |
Vetle Ledaal | 9688652e27 | |
Vetle Ledaal | 10542c7aa4 | |
alberlandohc | c8654953b2 | |
alberlandohc | 8a30734f59 | |
alberlandohc | cd90bcf6ca | |
alberlandohc | 4cc3316b5e | |
alberlandohc | 2b88f32401 | |
alberlandohc | 30e7a06b74 | |
Shahzaib | 2a44520bbe | |
SilverBeamx | 003545ac44 | |
Further | 5588f4d762 | |
are-are-are | bfe8f5d00f | |
Cuong-Tran | 7ec1dfaf48 | |
FourTOne5 | be9c14bcae | |
Vetle Ledaal | f03fd3c5f7 | |
kana-shii | 0382073769 | |
Creepler13 | 4f5116c590 | |
FourTOne5 | 931711fe74 | |
Cezary | 2430b18af0 | |
Vetle Ledaal | 8b212ffdcd | |
Chopper | c432620356 | |
Creepler13 | f228ad572d | |
Cuong-Tran | d65b847907 | |
Eshlender | f94b827056 | |
Eshlender | dfc8f73cb5 | |
Vetle Ledaal | d3054332eb |
|
@ -68,7 +68,7 @@ small, just do a normal full clone instead.**
|
|||
1. Do a partial clone.
|
||||
```bash
|
||||
git clone --filter=blob:none --sparse <fork-repo-url>
|
||||
cd extensions/
|
||||
cd extensions-source/
|
||||
```
|
||||
2. Configure sparse checkout.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -86,8 +86,7 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
|
|
@ -37,6 +37,8 @@ abstract class BlogTruyen(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
|
|
@ -814,8 +814,8 @@ abstract class GalleryAdults(
|
|||
val tags = mutableListOf<Genre>()
|
||||
runBlocking {
|
||||
val jobsPool = mutableListOf<Job>()
|
||||
// Get first 3 pages
|
||||
(1..3).forEach { page ->
|
||||
// Get first 5 pages
|
||||
(1..5).forEach { page ->
|
||||
jobsPool.add(
|
||||
launchIO {
|
||||
runCatching {
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 26
|
||||
baseVersionCode = 28
|
||||
|
|
|
@ -177,17 +177,18 @@ abstract class GroupLe(
|
|||
"div#tab-description .manga-description",
|
||||
).text()
|
||||
manga.status = when {
|
||||
infoElement.html()
|
||||
.contains("Запрещена публикация произведения по копирайту") || infoElement.html()
|
||||
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!") -> SManga.LICENSED
|
||||
infoElement.html().contains("<b>Сингл</b>") -> SManga.COMPLETED
|
||||
(
|
||||
document.html()
|
||||
.contains("Запрещена публикация произведения по копирайту") || document.html()
|
||||
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!")
|
||||
) && document.select("div.chapters").isEmpty() -> SManga.LICENSED
|
||||
infoElement.html().contains("<b>Сингл") -> SManga.COMPLETED
|
||||
else ->
|
||||
when (infoElement.select("p:contains(Перевод:) span").first()?.text()) {
|
||||
"продолжается" -> SManga.ONGOING
|
||||
"начат" -> SManga.ONGOING
|
||||
"переведено" -> SManga.COMPLETED
|
||||
"завершён" -> SManga.COMPLETED
|
||||
"приостановлен" -> SManga.ON_HIATUS
|
||||
when (infoElement.selectFirst("span.badge:contains(выпуск)")?.text()) {
|
||||
"выпуск продолжается" -> SManga.ONGOING
|
||||
"выпуск начат" -> SManga.ONGOING
|
||||
"выпуск завершён" -> if (infoElement.selectFirst("span.badge:contains(переведено)")?.text()?.isNotEmpty() == true) SManga.COMPLETED else SManga.PUBLISHING_FINISHED
|
||||
"выпуск приостановлен" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
@ -213,15 +214,9 @@ abstract class GroupLe(
|
|||
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
if ((
|
||||
document.select(".expandable.hide-dn").isNotEmpty() && document.select(".user-avatar")
|
||||
.isEmpty() && document.toString()
|
||||
.contains("current_user_country_code = 'RU'")
|
||||
) || (
|
||||
document.select("img.logo")
|
||||
.first()?.attr("title")
|
||||
?.contains("Allhentai") == true && document.select(".user-avatar").isEmpty()
|
||||
)
|
||||
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
|
@ -313,20 +308,22 @@ abstract class GroupLe(
|
|||
|
||||
val html = document.html()
|
||||
|
||||
val readerMark = "rm_h.readerDoInit(["
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
|
||||
if (!html.contains(readerMark)) {
|
||||
if (document.select(".input-lg").isNotEmpty() || (
|
||||
document.select(".user-avatar")
|
||||
.isEmpty() && document.select("img.logo").first()?.attr("title")
|
||||
?.contains("Allhentai") == true
|
||||
)
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
if (!response.request.url.toString().contains(baseUrl)) {
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
|
||||
val readerMark = when {
|
||||
html.contains("rm_h.readerDoInit([") -> "rm_h.readerDoInit(["
|
||||
html.contains("rm_h.readerInit([") -> "rm_h.readerInit(["
|
||||
!response.request.url.toString().contains(baseUrl) -> {
|
||||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
|
||||
}
|
||||
}
|
||||
|
||||
val beginIndex = html.indexOf(readerMark)
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
|
|
@ -33,7 +33,7 @@ abstract class Iken(
|
|||
.set("Referer", "$baseUrl/")
|
||||
|
||||
private var genres = emptyList<Pair<String, String>>()
|
||||
private val titleCache by lazy {
|
||||
protected val titleCache by lazy {
|
||||
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||
val data = response.parseAs<SearchResponse>()
|
||||
|
||||
|
@ -53,11 +53,9 @@ abstract class Iken(
|
|||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
||||
.map { it.absUrl("href").substringAfterLast("/series/") }
|
||||
|
||||
val entries = slugs.mapNotNull {
|
||||
titleCache[it]?.toSManga()
|
||||
val entries = document.select("aside a:has(img)").mapNotNull {
|
||||
titleCache[it.absUrl("href").substringAfter("series/")]?.toSManga()
|
||||
}
|
||||
|
||||
return MangasPage(entries, false)
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
||||
baseVersionCode = 12
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.asJsoup
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
|
@ -205,16 +206,22 @@ abstract class Keyoapp(
|
|||
}
|
||||
|
||||
// Details
|
||||
protected open val descriptionSelector: String = "div:containsOwn(Synopsis) ~ div"
|
||||
protected open val statusSelector: String = "div:has(span:containsOwn(Status)) ~ div"
|
||||
protected open val authorSelector: String = "div:has(span:containsOwn(Author)) ~ div"
|
||||
protected open val artistSelector: String = "div:has(span:containsOwn(Artist)) ~ div"
|
||||
protected open val genreSelector: String = "div:has(span:containsOwn(Type)) ~ div"
|
||||
protected open val dateSelector: String = ".text-xs"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.selectFirst("div.grid > h1")!!.text()
|
||||
thumbnail_url = document.getImageUrl("div[class*=photoURL]")
|
||||
description = document.selectFirst("div.grid > div.overflow-hidden > p")?.text()
|
||||
status = document.selectFirst("div[alt=Status]").parseStatus()
|
||||
author = document.selectFirst("div[alt=Author]")?.text()
|
||||
artist = document.selectFirst("div[alt=Artist]")?.text()
|
||||
description = document.selectFirst(descriptionSelector)?.text()
|
||||
status = document.selectFirst(statusSelector).parseStatus()
|
||||
author = document.selectFirst(authorSelector)?.text()
|
||||
artist = document.selectFirst(artistSelector)?.text()
|
||||
genre = buildList {
|
||||
document.selectFirst("div[alt='Series Type']")?.text()?.replaceFirstChar {
|
||||
document.selectFirst(genreSelector)?.text()?.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(
|
||||
Locale.getDefault(),
|
||||
|
@ -227,7 +234,7 @@ abstract class Keyoapp(
|
|||
}.joinToString()
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
protected fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"dropped" -> SManga.CANCELLED
|
||||
"paused" -> SManga.ON_HIATUS
|
||||
|
@ -247,7 +254,7 @@ abstract class Keyoapp(
|
|||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||
name = element.selectFirst(".text-sm")!!.text()
|
||||
element.selectFirst(".text-xs")?.run {
|
||||
element.selectFirst(dateSelector)?.run {
|
||||
date_upload = text().trim().parseDate()
|
||||
}
|
||||
if (element.select("img[src*=Coin.svg]").isNotEmpty()) {
|
||||
|
@ -308,6 +315,12 @@ abstract class Keyoapp(
|
|||
protected open fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let { element ->
|
||||
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
|
||||
?.toHttpUrlOrNull()?.let {
|
||||
it.newBuilder()
|
||||
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,8 +338,6 @@ abstract class Keyoapp(
|
|||
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val now = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 32
|
||||
baseVersionCode = 34
|
||||
|
|
|
@ -34,7 +34,6 @@ import kotlinx.serialization.json.Json
|
|||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
@ -65,43 +64,46 @@ abstract class LibGroup(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val userAgentMobile = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.3"
|
||||
|
||||
private var bearerToken: String? = null
|
||||
|
||||
private var userId: Int? = null
|
||||
|
||||
abstract val siteId: Int // Important in api calls
|
||||
|
||||
private val apiDomain: String = "https://api.lib.social"
|
||||
private val apiDomain: String = preferences.getString(API_DOMAIN_PREF, API_DOMAIN_DEFAULT).toString()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||
.connectTimeout(5, TimeUnit.MINUTES)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(::checkForToken)
|
||||
.addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == 419) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
|
||||
override val client by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 1)
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(::checkForToken)
|
||||
.addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == 419) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
if (response.code == 404) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E︎ и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
return@addInterceptor response
|
||||
}
|
||||
if (response.code == 404) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E︎ и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
return@addInterceptor response
|
||||
}
|
||||
.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
// User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
|
||||
add("User-Agent", userAgentMobile)
|
||||
add("Accept", "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
add("Referer", baseUrl)
|
||||
add("Site-Id", siteId.toString())
|
||||
}
|
||||
|
||||
private fun imageHeader() = Headers.Builder().apply {
|
||||
add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
add("Referer", baseUrl)
|
||||
}.build()
|
||||
|
||||
private var _constants: Constants? = null
|
||||
private fun getConstants(): Constants? {
|
||||
if (_constants == null) {
|
||||
|
@ -373,10 +375,25 @@ abstract class LibGroup(
|
|||
return chapter
|
||||
}
|
||||
|
||||
private fun checkImage(url: String): Boolean {
|
||||
val getUrlHead = Request.Builder().url(url).head().headers(imageHeader()).build()
|
||||
val response = client.newCall(getUrlHead).execute()
|
||||
return response.isSuccessful && (response.header("content-length", "0")?.toInt()!! > 600)
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
if (page.imageUrl != null) {
|
||||
return Observable.just(page.imageUrl)
|
||||
}
|
||||
if (isServer() == "auto") {
|
||||
for (serverApi in IMG_SERVERS.slice(1 until IMG_SERVERS.size)) {
|
||||
val server = getConstants()?.getServer(serverApi, siteId)?.url
|
||||
val imageUrl = "$server${page.url}"
|
||||
if (checkImage(imageUrl)) {
|
||||
return Observable.just(imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
val server = getConstants()?.getServer(isServer(), siteId)?.url ?: throw Exception("Ошибка получения сервера изображений")
|
||||
return Observable.just("$server${page.url}")
|
||||
}
|
||||
|
@ -384,13 +401,7 @@ abstract class LibGroup(
|
|||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imageHeader = Headers.Builder().apply {
|
||||
// User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
|
||||
add("User-Agent", userAgentMobile)
|
||||
add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
return GET(page.imageUrl!!, imageHeader.build())
|
||||
return GET(page.imageUrl!!, imageHeader())
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
|
@ -565,6 +576,7 @@ abstract class LibGroup(
|
|||
companion object {
|
||||
const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
private const val SERVER_PREF = "MangaLibImageServer"
|
||||
private val IMG_SERVERS = arrayOf("auto", "main", "secondary", "compress")
|
||||
|
||||
private const val SORTING_PREF = "MangaLibSorting"
|
||||
private const val SORTING_PREF_TITLE = "Способ выбора переводчиков"
|
||||
|
@ -578,12 +590,16 @@ abstract class LibGroup(
|
|||
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
|
||||
private const val LANGUAGE_PREF_TITLE = "Выбор языка на обложке"
|
||||
|
||||
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
|
||||
private const val API_DOMAIN_TITLE = "Выбор домена API"
|
||||
private const val API_DOMAIN_DEFAULT = "https://api.imglib.info"
|
||||
|
||||
private const val TOKEN_STORE = "TokenStore"
|
||||
|
||||
val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US) }
|
||||
}
|
||||
|
||||
private fun isServer(): String = preferences.getString(SERVER_PREF, "main")!!
|
||||
private fun isServer(): String = preferences.getString(SERVER_PREF, "compress")!!
|
||||
private fun isEng(): String = preferences.getString(LANGUAGE_PREF, "eng")!!
|
||||
private fun groupTranslates(): String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
|
||||
private fun isScanUser(): Boolean = preferences.getBoolean(IS_SCAN_USER, false)
|
||||
|
@ -591,12 +607,18 @@ abstract class LibGroup(
|
|||
val serverPref = ListPreference(screen.context).apply {
|
||||
key = SERVER_PREF
|
||||
title = "Сервер изображений"
|
||||
entries = arrayOf("Первый", "Второй", "Сжатия")
|
||||
entryValues = arrayOf("main", "secondary", "compress")
|
||||
summary = "%s \n\nВыбор приоритетного сервера изображений. \n" +
|
||||
"По умолчанию «Первый». \n\n" +
|
||||
entries = arrayOf("Автовыбор", "Первый", "Второй", "Сжатия")
|
||||
entryValues = IMG_SERVERS
|
||||
summary = "%s \n\n" +
|
||||
"По умолчанию в приложении и на сайте «Сжатия» - самый стабильный и быстрый. \n\n" +
|
||||
"«Автовыбор» - проходит по всем серверам и показывает только загруженную картинку. \nМожет происходить медленно. \n\n" +
|
||||
"ⓘВыбор другого сервера помогает при ошибках и медленной загрузки изображений глав."
|
||||
setDefaultValue("main")
|
||||
setDefaultValue("compress")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val warning = "Для смены сервера: Настройки -> Дополнительно -> Очистить кэш глав"
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val sortingPref = ListPreference(screen.context).apply {
|
||||
|
@ -629,11 +651,30 @@ abstract class LibGroup(
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
val domainApiPref = ListPreference(screen.context).apply {
|
||||
key = API_DOMAIN_PREF
|
||||
title = API_DOMAIN_TITLE
|
||||
entries = arrayOf("Официальное приложение (api.imglib.info)", "Основной (api.lib.social)", "Резервный (api.mangalib.me)", "Резервный 2 (api2.mangalib.me)")
|
||||
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api.lib.social", "https://api.mangalib.me", "https://api2.mangalib.me")
|
||||
summary = "%s" +
|
||||
"\n\nВыбор домена API, используемого для работы приложения." +
|
||||
"\n\nПо умолчанию «Официальное приложение»" +
|
||||
"\n\nⓘВы не увидите его нигде глазами, но источник должен начать работать стибильнее."
|
||||
setDefaultValue(API_DOMAIN_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(serverPref)
|
||||
screen.addPreference(sortingPref)
|
||||
screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates()))
|
||||
screen.addPreference(scanlatorUsername)
|
||||
screen.addPreference(titleLanguagePref)
|
||||
screen.addPreference(domainApiPref)
|
||||
}
|
||||
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String): androidx.preference.EditTextPreference {
|
||||
return androidx.preference.EditTextPreference(context).apply {
|
||||
|
|
|
@ -45,7 +45,7 @@ class Constants(
|
|||
)
|
||||
|
||||
fun getServer(isServers: String?, siteId: Int): ImageServer =
|
||||
if (!isServers.isNullOrBlank()) {
|
||||
if (!isServers.isNullOrBlank() and (isServers != "auto")) {
|
||||
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
|
||||
} else {
|
||||
imageServers.first { it.siteIds.contains(siteId) }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<application>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.multisrc.mangasee.MangaseeUrlActivity"
|
||||
android:name="eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslationsUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
@ -12,11 +12,10 @@
|
|||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="mangasee123.com"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/.*/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
||||
baseVersionCode = 2
|
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 45 KiB |
|
@ -1,9 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.en.snowmtl
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -24,22 +24,21 @@ import java.text.SimpleDateFormat
|
|||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class Snowmtl : ParsedHttpSource() {
|
||||
|
||||
override val name = "Snow Machine Translations"
|
||||
|
||||
override val baseUrl = "https://snowmtl.ru"
|
||||
|
||||
override val lang = "en"
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
abstract class MachineTranslations(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
val language: Language,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val lang = language.lang
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.addInterceptor(ComposedImageInterceptor(baseUrl, super.client))
|
||||
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
|
||||
.build()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
@ -158,7 +157,9 @@ class Snowmtl : ParsedHttpSource() {
|
|||
dto.imageUrl.startsWith("http") -> dto.imageUrl
|
||||
else -> "https://${dto.imageUrl}"
|
||||
}
|
||||
val fragment = json.encodeToString<List<Translation>>(dto.translations)
|
||||
val fragment = json.encodeToString<List<Dialog>>(
|
||||
dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
|
||||
)
|
||||
Page(index, imageUrl = "$imageUrl#$fragment")
|
||||
}
|
||||
}
|
||||
|
@ -203,6 +204,7 @@ class Snowmtl : ParsedHttpSource() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.snowmtl
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
|
@ -8,35 +8,43 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import java.io.IOException
|
||||
|
||||
@Serializable
|
||||
class PageDto(
|
||||
@SerialName("img_url")
|
||||
val imageUrl: String,
|
||||
@Serializable(with = TranslationsListSerializer::class)
|
||||
val translations: List<Translation> = emptyList(),
|
||||
|
||||
@SerialName("translations")
|
||||
@Serializable(with = DialogListSerializer::class)
|
||||
val dialogues: List<Dialog> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class Translation(
|
||||
data class Dialog(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float,
|
||||
val text: String,
|
||||
val angle: Float = 0f,
|
||||
val isBold: Boolean = false,
|
||||
val isNewApi: Boolean = false,
|
||||
val type: String = "sub",
|
||||
val textByLanguage: Map<String, String> = emptyMap(),
|
||||
val type: String = "normal",
|
||||
private val fbColor: List<Int> = emptyList(),
|
||||
private val bgColor: List<Int> = emptyList(),
|
||||
) {
|
||||
val text: String get() = textByLanguage["text"] ?: throw Exception("Dialog not found")
|
||||
fun getTextBy(language: Language) = textByLanguage[language.target] ?: text
|
||||
|
||||
val width get() = x2 - x1
|
||||
val height get() = y2 - y1
|
||||
val centerY get() = (y2 + y1) / 2f
|
||||
|
@ -55,41 +63,59 @@ class Translation(
|
|||
}
|
||||
}
|
||||
|
||||
private object TranslationsListSerializer :
|
||||
JsonTransformingSerializer<List<Translation>>(ListSerializer(Translation.serializer())) {
|
||||
private object DialogListSerializer :
|
||||
JsonTransformingSerializer<List<Dialog>>(ListSerializer(Dialog.serializer())) {
|
||||
override fun transformDeserialize(element: JsonElement): JsonElement {
|
||||
return JsonArray(
|
||||
element.jsonArray.map { jsonElement ->
|
||||
val (coordinates, text) = getCoordinatesAndCaption(jsonElement)
|
||||
val coordinates = getCoordinates(jsonElement)
|
||||
val textByLanguage = getDialogs(jsonElement)
|
||||
|
||||
buildJsonObject {
|
||||
put("x1", coordinates[0])
|
||||
put("y1", coordinates[1])
|
||||
put("x2", coordinates[2])
|
||||
put("y2", coordinates[3])
|
||||
put("text", text)
|
||||
put("textByLanguage", textByLanguage)
|
||||
|
||||
try {
|
||||
val obj = jsonElement.jsonObject
|
||||
if (jsonElement.isArray) {
|
||||
return@buildJsonObject
|
||||
}
|
||||
|
||||
jsonElement.jsonObject.let { obj ->
|
||||
obj["fg_color"]?.let { put("fbColor", it) }
|
||||
obj["bg_color"]?.let { put("bgColor", it) }
|
||||
obj["angle"]?.let { put("angle", it) }
|
||||
obj["type"]?.let { put("type", it) }
|
||||
obj["is_bold"]?.let { put("isBold", it) }
|
||||
put("isNewApi", true)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCoordinatesAndCaption(element: JsonElement): Pair<JsonArray, JsonElement> {
|
||||
return try {
|
||||
val arr = element.jsonArray
|
||||
arr[0].jsonArray to arr[1]
|
||||
} catch (_: Exception) {
|
||||
val obj = element.jsonObject
|
||||
obj["bbox"]!!.jsonArray to obj["text"]!!
|
||||
private fun getCoordinates(element: JsonElement): JsonArray {
|
||||
return when (element) {
|
||||
is JsonArray -> element.jsonArray[0].jsonArray
|
||||
else -> element.jsonObject["bbox"]?.jsonArray
|
||||
?: throw IOException("Dialog box position not found")
|
||||
}
|
||||
}
|
||||
private fun getDialogs(element: JsonElement): JsonObject {
|
||||
return buildJsonObject {
|
||||
when (element) {
|
||||
is JsonArray -> put("text", element.jsonArray[1])
|
||||
else -> {
|
||||
element.jsonObject.entries
|
||||
.filter { it.value.isString }
|
||||
.forEach { put(it.key, it.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val JsonElement.isArray get() = this is JsonArray
|
||||
private val JsonElement.isObject get() = this is JsonObject
|
||||
private val JsonElement.isString get() = this.isObject.not() && this.isArray.not() && this.jsonPrimitive.isString
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
class MachineTranslationsFactoryUtils
|
||||
|
||||
data class Language(val lang: String, val target: String = lang, val origin: String = "en")
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.snowmtl
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
package eu.kanade.tachiyomi.extension.es.tumanhwas
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class TuManhwasUrlActivity : Activity() {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class MachineTranslationsUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
|
@ -18,7 +20,7 @@ class TuManhwasUrlActivity : Activity() {
|
|||
val item = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${TuManhwas.URL_SEARCH_PREFIX}$item")
|
||||
putExtra("query", "${MachineTranslations.PREFIX_SEARCH}$item")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.snowmtl
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
|
@ -12,12 +12,14 @@ import android.text.Layout
|
|||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.annotation.RequiresApi
|
||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog
|
||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
@ -29,11 +31,11 @@ import java.io.InputStream
|
|||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
// The Interceptor joins the captions and pages of the manga.
|
||||
// The Interceptor joins the dialogues and pages of the manga.
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class ComposedImageInterceptor(
|
||||
baseUrl: String,
|
||||
private val client: OkHttpClient,
|
||||
val language: Language,
|
||||
) : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
@ -44,48 +46,42 @@ class ComposedImageInterceptor(
|
|||
"normal" to Pair<String, Typeface?>("$baseUrl/images/normal.ttf", null),
|
||||
)
|
||||
|
||||
private val imageRegex = Regex(
|
||||
"$baseUrl.*?\\.(webp|png|jpg|jpeg)#\\[.*?]",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val url = request.url.toString()
|
||||
|
||||
val isPageImageUrl = imageRegex.containsMatchIn(url)
|
||||
if (isPageImageUrl.not()) {
|
||||
if (PAGE_REGEX.containsMatchIn(url).not()) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
val translation = request.url.fragment?.parseAs<List<Translation>>()
|
||||
?: throw IOException("Translation not found")
|
||||
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
|
||||
?: throw IOException("Dialogues not found")
|
||||
|
||||
val imageRequest = request.newBuilder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
// Load the fonts before opening the connection to load the image,
|
||||
// so there aren't two open connections inside the interceptor.
|
||||
loadAllFont(chain)
|
||||
|
||||
val response = chain.proceed(imageRequest)
|
||||
|
||||
if (response.isSuccessful.not()) {
|
||||
return response
|
||||
}
|
||||
|
||||
loadAllFont(chain)
|
||||
|
||||
val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!!
|
||||
.copy(Bitmap.Config.ARGB_8888, true)
|
||||
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
translation
|
||||
.filter { it.text.isNotBlank() }
|
||||
.forEach { caption ->
|
||||
val textPaint = createTextPaint(selectFontFamily(caption.type))
|
||||
val dialogBox = createDialogBox(caption, textPaint, bitmap)
|
||||
val y = getYAxis(textPaint, caption, dialogBox)
|
||||
canvas.draw(dialogBox, caption, caption.x1, y)
|
||||
}
|
||||
dialogues.forEach { dialog ->
|
||||
val textPaint = createTextPaint(selectFontFamily(dialog.type))
|
||||
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
|
||||
val y = getYAxis(textPaint, dialog, dialogBox)
|
||||
canvas.draw(dialogBox, dialog, dialog.x1, y)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
|
||||
|
@ -108,7 +104,7 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
private fun createTextPaint(font: Typeface?): TextPaint {
|
||||
val defaultTextSize = 50.sp // arbitrary
|
||||
val defaultTextSize = 24.pt // arbitrary
|
||||
return TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = defaultTextSize
|
||||
|
@ -131,12 +127,13 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
private fun loadAllFont(chain: Interceptor.Chain) {
|
||||
val fallback = loadFont("coming_soon_regular.ttf")
|
||||
fontFamily.keys.forEach { key ->
|
||||
val font = fontFamily[key] ?: return@forEach
|
||||
if (font.second != null) {
|
||||
return@forEach
|
||||
}
|
||||
fontFamily[key] = key to (loadRemoteFont(font.first, chain) ?: loadFont("coming_soon_regular.ttf"))
|
||||
fontFamily[key] = key to (loadRemoteFont(font.first, chain) ?: fallback)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,11 +167,17 @@ class ComposedImageInterceptor(
|
|||
private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? {
|
||||
return try {
|
||||
val request = GET(fontUrl, chain.request().headers)
|
||||
val response = client
|
||||
.newCall(request).execute()
|
||||
.takeIf(Response::isSuccessful) ?: return null
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (response.isSuccessful.not()) {
|
||||
response.close()
|
||||
return null
|
||||
}
|
||||
|
||||
val fontName = request.url.pathSegments.last()
|
||||
response.body.byteStream().toTypeface(fontName)
|
||||
response.body.use {
|
||||
it.byteStream().toTypeface(fontName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -189,63 +192,66 @@ class ComposedImageInterceptor(
|
|||
/**
|
||||
* Adjust the text to the center of the dialog box when feasible.
|
||||
*/
|
||||
private fun getYAxis(textPaint: TextPaint, caption: Translation, dialogBox: StaticLayout): Float {
|
||||
private fun getYAxis(textPaint: TextPaint, dialog: Dialog, dialogBox: StaticLayout): Float {
|
||||
val fontHeight = textPaint.fontMetrics.let { it.bottom - it.top }
|
||||
|
||||
val dialogBoxLineCount = caption.height / fontHeight
|
||||
val dialogBoxLineCount = dialog.height / fontHeight
|
||||
|
||||
/**
|
||||
* Centers text in y for captions smaller than the dialog box
|
||||
* Centers text in y for dialogues smaller than the dialog box
|
||||
*/
|
||||
return when {
|
||||
dialogBox.lineCount < dialogBoxLineCount -> caption.centerY - dialogBox.lineCount / 2f * fontHeight
|
||||
else -> caption.y1
|
||||
dialogBox.lineCount < dialogBoxLineCount -> dialog.centerY - dialogBox.lineCount / 2f * fontHeight
|
||||
else -> dialog.y1
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDialogBox(caption: Translation, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
|
||||
var dialogBox = createBoxLayout(caption, textPaint)
|
||||
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
|
||||
var dialogBox = createBoxLayout(dialog, textPaint)
|
||||
|
||||
/**
|
||||
* The best way I've found to adjust the text in the dialog box (Especially in long dialogues)
|
||||
*/
|
||||
while (dialogBox.height > caption.height) {
|
||||
while (dialogBox.height > dialog.height) {
|
||||
textPaint.textSize -= 0.5f
|
||||
dialogBox = createBoxLayout(caption, textPaint)
|
||||
dialogBox = createBoxLayout(dialog, textPaint)
|
||||
}
|
||||
|
||||
// Use source setup
|
||||
if (caption.isNewApi) {
|
||||
textPaint.color = caption.foregroundColor
|
||||
textPaint.bgColor = caption.backgroundColor
|
||||
textPaint.style = if (caption.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
|
||||
if (dialog.isNewApi) {
|
||||
textPaint.color = dialog.foregroundColor
|
||||
textPaint.bgColor = dialog.backgroundColor
|
||||
textPaint.style = if (dialog.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces font color correction if the background color of the dialog box and the font color are too similar.
|
||||
* It's a source configuration problem.
|
||||
*/
|
||||
textPaint.adjustTextColor(caption, bitmap)
|
||||
textPaint.adjustTextColor(dialog, bitmap)
|
||||
|
||||
return dialogBox
|
||||
}
|
||||
|
||||
private fun createBoxLayout(caption: Translation, textPaint: TextPaint) =
|
||||
StaticLayout.Builder.obtain(caption.text, 0, caption.text.length, textPaint, caption.width.toInt()).apply {
|
||||
private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint): StaticLayout {
|
||||
val text = dialog.getTextBy(language)
|
||||
|
||||
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, dialog.width.toInt()).apply {
|
||||
setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
setIncludePad(false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
// Invert color in black dialog box.
|
||||
private fun TextPaint.adjustTextColor(caption: Translation, bitmap: Bitmap) {
|
||||
val pixelColor = bitmap.getPixel(caption.centerX.toInt(), caption.centerY.toInt())
|
||||
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {
|
||||
val pixelColor = bitmap.getPixel(dialog.centerX.toInt(), dialog.centerY.toInt())
|
||||
val inverseColor = (Color.WHITE - pixelColor) or Color.BLACK
|
||||
|
||||
val minDistance = 80f // arbitrary
|
||||
if (colorDistance(pixelColor, caption.foregroundColor) > minDistance) {
|
||||
if (colorDistance(pixelColor, dialog.foregroundColor) > minDistance) {
|
||||
return
|
||||
}
|
||||
color = inverseColor
|
||||
|
@ -255,15 +261,16 @@ class ComposedImageInterceptor(
|
|||
return json.decodeFromString(this)
|
||||
}
|
||||
|
||||
private fun Canvas.draw(layout: StaticLayout, caption: Translation, x: Float, y: Float) {
|
||||
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
|
||||
save()
|
||||
translate(x, y)
|
||||
rotate(caption.angle)
|
||||
rotate(dialog.angle)
|
||||
layout.draw(this)
|
||||
restore()
|
||||
}
|
||||
|
||||
private val Int.sp: Float get() = this * SCALED_DENSITY
|
||||
// https://pixelsconverter.com/pt-to-px
|
||||
private val Int.pt: Float get() = this / SCALED_DENSITY
|
||||
|
||||
// ============================= Utils ======================================
|
||||
|
||||
|
@ -288,7 +295,8 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val SCALED_DENSITY = 1.5f // arbitrary
|
||||
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
|
||||
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt
|
||||
val mediaType = "image/png".toMediaType()
|
||||
}
|
||||
}
|
|
@ -46,6 +46,8 @@ abstract class MangaEsp(
|
|||
|
||||
protected open val seriesPath = "/ver"
|
||||
|
||||
protected open val useApiSearch = false
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
|
@ -62,7 +64,9 @@ abstract class MangaEsp(
|
|||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga(seriesPath) }
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.additionalParse()
|
||||
.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
@ -72,7 +76,9 @@ abstract class MangaEsp(
|
|||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
||||
|
||||
val mangas = responseData.response.map { it.toSManga(seriesPath) }
|
||||
val mangas = responseData.response
|
||||
.additionalParse()
|
||||
.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
@ -93,20 +99,33 @@ abstract class MangaEsp(
|
|||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers)
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (useApiSearch) {
|
||||
GET("$apiBaseUrl$apiPath/comics", headers)
|
||||
} else {
|
||||
GET("$baseUrl/comics", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
protected open fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
||||
comicsList = if (useApiSearch) {
|
||||
json.decodeFromString<List<SeriesDto>>(response.body.string()).toMutableList()
|
||||
} else {
|
||||
val script = response.asJsoup().select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
||||
}.additionalParse().toMutableList()
|
||||
return parseComicsList(page, query, filters)
|
||||
}
|
||||
|
||||
protected open fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
||||
return this
|
||||
}
|
||||
|
||||
private var filteredList = mutableListOf<SeriesDto>()
|
||||
|
||||
protected open fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
baseVersionCode = 29
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
|
|
|
@ -50,7 +50,7 @@ abstract class MangaHub(
|
|||
private var baseApiUrl = "https://api.mghcdn.com"
|
||||
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
userAgentType = UserAgentType.DESKTOP,
|
||||
filterInclude = listOf("chrome"),
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
|
|
@ -22,6 +22,8 @@ abstract class MangaReader : HttpSource(), ConfigurableSource {
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
|
|
@ -30,3 +30,5 @@ project_filter_warning=NOTE: Can't be used with other filter!
|
|||
project_filter_name=%s Project List page
|
||||
pref_dynamic_url_title=Automatically update dynamic URLs
|
||||
pref_dynamic_url_summary=Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and "in library" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source.
|
||||
pref_hide_paid_chapters_title=Hide chapters which require a purchase
|
||||
pref_hide_paid_chapters_summary=Hide chapters which must be purchased using coins.\nYou might want to disable this if you want to be notified of paid chapters so that you can go purchase them.
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
|
||||
class MangaThemesiaPaidChapterHelper(
|
||||
private val hidePaidChaptersPrefKey: String = "pref_hide_paid_chapters",
|
||||
private val lockedChapterSelector: String = "a[data-bs-target='#lockedChapterModal']",
|
||||
) {
|
||||
fun addHidePaidChaptersPreferenceToScreen(screen: PreferenceScreen, intl: Intl) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = hidePaidChaptersPrefKey
|
||||
title = intl["pref_hide_paid_chapters_title"]
|
||||
summary = intl["pref_hide_paid_chapters_summary"]
|
||||
setDefaultValue(true)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
fun getHidePaidChaptersPref(preferences: SharedPreferences) = preferences.getBoolean(hidePaidChaptersPrefKey, true)
|
||||
|
||||
fun getChapterListSelectorBasedOnHidePaidChaptersPref(baseChapterListSelector: String, preferences: SharedPreferences): String {
|
||||
if (!getHidePaidChaptersPref(preferences)) {
|
||||
return baseChapterListSelector
|
||||
}
|
||||
|
||||
// Fragile
|
||||
val selectors = baseChapterListSelector.split(", ")
|
||||
|
||||
return selectors
|
||||
.map { "$it:not($lockedChapterSelector):not(:has($lockedChapterSelector))" }
|
||||
.joinToString()
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 11
|
||||
baseVersionCode = 12
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -67,6 +67,8 @@ constructor(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
|
|
|
@ -1,428 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.nepnep
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Source responds to requests with their full database as a JsonArray, then sorts/filters it client-side
|
||||
* We'll take the database on first requests, then do what we want with it
|
||||
*/
|
||||
abstract class NepNep(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/77.0")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private lateinit var directory: List<JsonElement>
|
||||
|
||||
// Convenience functions to shorten later code
|
||||
/** Returns value corresponding to given key as a string, or null */
|
||||
private fun JsonElement.getString(key: String): String? {
|
||||
return this.jsonObject[key]!!.jsonPrimitive.contentOrNull
|
||||
}
|
||||
|
||||
/** Returns value corresponding to given key as a JsonArray */
|
||||
private fun JsonElement.getArray(key: String): JsonArray {
|
||||
return this.jsonObject[key]!!.jsonArray
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search/", headers)
|
||||
}
|
||||
|
||||
// don't use ";" for substringBefore() !
|
||||
private fun directoryFromDocument(document: Document): JsonArray {
|
||||
val str = document.select("script:containsData(MainFunction)").first()!!.data()
|
||||
.substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim()
|
||||
.replace(";", " ")
|
||||
return json.parseToJsonElement(str).jsonArray
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
thumbnailUrl = document.select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
|
||||
directory = directoryFromDocument(document).sortedByDescending { it.getString("v") }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
private fun parseDirectory(page: Int): MangasPage {
|
||||
val mangas = mutableListOf<SManga>()
|
||||
val endRange = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
|
||||
|
||||
for (i in (((page - 1) * 24)..endRange)) {
|
||||
mangas.add(
|
||||
SManga.create().apply {
|
||||
title = directory[i].getString("s")!!
|
||||
url = "/manga/${directory[i].getString("i")}"
|
||||
thumbnail_url = getThumbnailUrl(directory[i].getString("i")!!)
|
||||
},
|
||||
)
|
||||
}
|
||||
return MangasPage(mangas, endRange < directory.lastIndex)
|
||||
}
|
||||
|
||||
private var thumbnailUrl: String? = null
|
||||
|
||||
private fun getThumbnailUrl(id: String): String {
|
||||
if (thumbnailUrl.isNullOrEmpty()) {
|
||||
val response = client.newCall(popularMangaRequest(1)).execute()
|
||||
thumbnailUrl = response.asJsoup().select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
|
||||
}
|
||||
|
||||
return thumbnailUrl!!.replace("{{Result.i}}", id)
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(1)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
directory = directoryFromDocument(response.asJsoup()).sortedByDescending { it.getString("lt") }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, query, filters)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(1)
|
||||
|
||||
private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage {
|
||||
val trimmedQuery = query.trim()
|
||||
directory = directoryFromDocument(response.asJsoup())
|
||||
.filter {
|
||||
// Comparing query with display name
|
||||
it.getString("s")!!.contains(trimmedQuery, ignoreCase = true) or
|
||||
// Comparing query with list of alternate names
|
||||
it.getArray("al").any { altName ->
|
||||
altName.jsonPrimitive.content.contains(trimmedQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
var sortBy: String
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> {
|
||||
sortBy = when (filter.state?.index) {
|
||||
1 -> "ls"
|
||||
2 -> "v"
|
||||
else -> "s"
|
||||
}
|
||||
directory = if (filter.state?.ascending != true) {
|
||||
directory.sortedByDescending { it.getString(sortBy) }
|
||||
} else {
|
||||
directory.sortedByDescending { it.getString(sortBy) }.reversed()
|
||||
}
|
||||
}
|
||||
is SelectField -> if (filter.state != 0) {
|
||||
directory = when (filter.name) {
|
||||
"Scan Status" -> directory.filter { it.getString("ss")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Publish Status" -> directory.filter { it.getString("ps")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Type" -> directory.filter { it.getString("t")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Translation" -> directory.filter { it.getString("o")!!.contains("yes", ignoreCase = true) }
|
||||
else -> directory
|
||||
}
|
||||
}
|
||||
is YearField -> if (filter.state.isNotEmpty()) directory = directory.filter { it.getString("y")!!.contains(filter.state) }
|
||||
is AuthorField -> if (filter.state.isNotEmpty()) {
|
||||
directory = directory.filter { e ->
|
||||
e.getArray("a").any {
|
||||
it.jsonPrimitive.content.contains(filter.state, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||
}
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) {
|
||||
genres.map { genre ->
|
||||
directory = directory.filter { e ->
|
||||
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genresNo.isNotEmpty()) {
|
||||
genresNo.map { genre ->
|
||||
directory = directory.filterNot { e ->
|
||||
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return response.asJsoup().select("div.BoxBody > div.row").let { info ->
|
||||
SManga.create().apply {
|
||||
title = info.select("h1").text()
|
||||
author = info.select("li.list-group-item:has(span:contains(Author)) a").first()?.text()
|
||||
status = info.select("li.list-group-item:has(span:contains(Status)) a:contains(scan)").text().toStatus()
|
||||
description = info.select("div.Content").text()
|
||||
thumbnail_url = info.select("img").attr("abs:src")
|
||||
|
||||
val genres = info.select("li.list-group-item:has(span:contains(Genre)) a")
|
||||
.map { element -> element.text() }
|
||||
.toMutableSet()
|
||||
|
||||
// add series type(manga/manhwa/manhua/other) thinggy to genre
|
||||
info.select("li.list-group-item:has(span:contains(Type)) a, a[href*=type\\=]").firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not()) {
|
||||
genres.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
genre = genres.toList().joinToString(", ")
|
||||
|
||||
// add alternative name to manga description
|
||||
val altName = "Alternative Name: "
|
||||
info.select("li.list-group-item:has(span:contains(Alter))").firstOrNull()?.ownText()?.let {
|
||||
if (it.isBlank().not() && it != "N/A") {
|
||||
description = when {
|
||||
description.isNullOrBlank() -> altName + it
|
||||
else -> description + "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toStatus() = when {
|
||||
this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED
|
||||
this.contains("Cancelled", ignoreCase = true) -> SManga.CANCELLED
|
||||
this.contains("Hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters - Mind special cases like decimal chapters (e.g. One Punch Man) and manga with seasons (e.g. The Gamer)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS Z", Locale.getDefault())
|
||||
|
||||
private fun chapterURLEncode(e: String): String {
|
||||
var index = ""
|
||||
val t = e.substring(0, 1).toInt()
|
||||
if (1 != t) { index = "-index-$t" }
|
||||
val dgt = if (e.toInt() < 100100) { 4 } else if (e.toInt() < 101000) { 3 } else if (e.toInt() < 110000) { 2 } else { 1 }
|
||||
val n = e.substring(dgt, e.length - 1)
|
||||
var suffix = ""
|
||||
val path = e.substring(e.length - 1).toInt()
|
||||
if (0 != path) { suffix = ".$path" }
|
||||
return "-chapter-$n$suffix$index.html"
|
||||
}
|
||||
|
||||
private val chapterImageRegex = Regex("""^0+""")
|
||||
|
||||
private fun chapterImage(e: String, cleanString: Boolean = false): String {
|
||||
// cleanString will result in an empty string if chapter number is 0, hence the else if below
|
||||
val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it }
|
||||
// If b is not zero, indicates chapter has decimal numbering
|
||||
val b = e.substring(e.length - 1).toInt()
|
||||
return if (b == 0 && a.isNotEmpty()) {
|
||||
a
|
||||
} else if (b == 0 && a.isEmpty()) {
|
||||
"0"
|
||||
} else {
|
||||
"$a.$b"
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val vmChapters = response.asJsoup().select("script:containsData(MainFunction)").first()!!.data()
|
||||
.substringAfter("vm.Chapters = ").substringBefore(";")
|
||||
val array = json.parseToJsonElement(vmChapters).jsonArray
|
||||
val hasDistinctTypes = array.map { it.getString("Type") }.distinct().count() > 1
|
||||
return array.map { json ->
|
||||
val indexChapter = json.getString("Chapter")!!
|
||||
val type = json.getString("Type")
|
||||
SChapter.create().apply {
|
||||
name = json.getString("ChapterName").let { if (it.isNullOrEmpty()) "$type ${chapterImage(indexChapter, true)}" else it }
|
||||
url = "/read-online/" + response.request.url.toString().substringAfter("/manga/") + chapterURLEncode(indexChapter)
|
||||
// only add type info as scanlator if there are differing types among chapter array
|
||||
scanlator = if (hasDistinctTypes) type else null
|
||||
date_upload = try {
|
||||
json.getString("Date").let { dateFormat.parse("$it +0600")?.time } ?: 0
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst("script:containsData(MainFunction)")?.data()
|
||||
?: client.newCall(GET(document.location().removeSuffix(".html"), headers))
|
||||
.execute().asJsoup().selectFirst("script:containsData(MainFunction)")!!.data()
|
||||
val curChapter = json.parseToJsonElement(script!!.substringAfter("vm.CurChapter = ").substringBefore(";")).jsonObject
|
||||
|
||||
val pageTotal = curChapter.getString("Page")!!.toInt()
|
||||
|
||||
val host = "https://" +
|
||||
script
|
||||
.substringAfter("vm.CurPathName = \"", "")
|
||||
.substringBefore("\"")
|
||||
.also {
|
||||
if (it.isEmpty()) {
|
||||
throw Exception("$name is overloaded and blocking Tachiyomi right now. Wait for unblock.")
|
||||
}
|
||||
}
|
||||
val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"")
|
||||
val seasonURI = curChapter.getString("Directory")!!
|
||||
.let { if (it.isEmpty()) "" else "$it/" }
|
||||
val path = "$host/manga/$titleURI/$seasonURI"
|
||||
|
||||
val chNum = chapterImage(curChapter.getString("Chapter")!!)
|
||||
|
||||
return IntRange(1, pageTotal).mapIndexed { i, _ ->
|
||||
val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) }
|
||||
Page(i, "", "$path$chNum-$imageNum.png")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Selection(2, false))
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class YearField : Filter.Text("Years")
|
||||
private class AuthorField : Filter.Text("Author")
|
||||
private class SelectField(name: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
YearField(),
|
||||
AuthorField(),
|
||||
SelectField("Scan Status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
SelectField("Publish Status", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
SelectField("Type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
SelectField("Translation", arrayOf("Any", "Official Only")),
|
||||
Sort(),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// https://manga4life.com/advanced-search/
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Hentai"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Isekai"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri"),
|
||||
)
|
||||
}
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -29,6 +29,8 @@ abstract class PizzaReader(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
open val apiUrl by lazy { "$baseUrl$apiPath" }
|
||||
|
||||
protected open val json: Json by injectLazy()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".pt.slimeread.SlimeReadUrlActivity"
|
||||
android:name="eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadThemeUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
@ -12,10 +12,10 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="slimeread.com"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,273 @@
|
|||
package eu.kanade.tachiyomi.multisrc.slimereadtheme
|
||||
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.ChapterDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.LatestResponseDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.MangaInfoDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PageListDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PopularMangaDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.toSMangaList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
abstract class SlimeReadTheme(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val scanId: String = "",
|
||||
) : HttpSource() {
|
||||
|
||||
protected open val apiUrl: String by lazy { getApiUrlFromPage() }
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
protected open val urlInfix: String = "slimeread.com"
|
||||
|
||||
protected open fun getApiUrlFromPage(): String {
|
||||
val initClient = network.cloudflareClient
|
||||
val response = initClient.newCall(GET(baseUrl, headers)).execute()
|
||||
if (!response.isSuccessful) throw Exception("HTTP error ${response.code}")
|
||||
val document = response.asJsoup()
|
||||
val scriptUrl = document.selectFirst("script[src*=pages/_app]")?.attr("abs:src")
|
||||
?: throw Exception("Could not find script URL")
|
||||
val scriptResponse = initClient.newCall(GET(scriptUrl, headers)).execute()
|
||||
if (!scriptResponse.isSuccessful) throw Exception("HTTP error ${scriptResponse.code}")
|
||||
val script = scriptResponse.body.string()
|
||||
val apiUrl = FUNCTION_REGEX.find(script)?.let { result ->
|
||||
val varBlock = result.groupValues[1]
|
||||
val varUrlInfix = result.groupValues[2]
|
||||
|
||||
val block = """${varBlock.replace(varUrlInfix, "\"$urlInfix\"")}.toString()"""
|
||||
|
||||
try {
|
||||
QuickJs.create().use { it.evaluate(block) as String }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return apiUrl?.let { "https://$it" } ?: throw Exception("Could not find API URL")
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
private var popularMangeCache: MangasPage? = null
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$apiUrl/book_search?order=1&status=0".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
popularMangeCache = popularMangeCache?.takeIf { page != 1 }
|
||||
?: super.fetchPopularManga(page).toBlocking().last()
|
||||
return pageableOf(page, popularMangeCache!!)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val items = response.parseAs<List<PopularMangaDto>>()
|
||||
val mangaList = items.toSMangaList()
|
||||
return MangasPage(mangaList, mangaList.isNotEmpty())
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$apiUrl/books?page=$page".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val dto = response.parseAs<LatestResponseDto>()
|
||||
val mangaList = dto.data.toSMangaList()
|
||||
val hasNextPage = dto.page < dto.pages
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
private var searchMangaCache: MangasPage? = null
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$apiUrl/book/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map(::searchMangaByIdParse)
|
||||
} else {
|
||||
searchMangaCache = searchMangaCache?.takeIf { page != 1 }
|
||||
?: super.fetchSearchManga(page, query, filters).toBlocking().last()
|
||||
pageableOf(page, searchMangaCache!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaByIdParse(response: Response): MangasPage {
|
||||
val details = mangaDetailsParse(response)
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun getFilterList() = SlimeReadThemeFilters.FILTER_LIST
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val params = SlimeReadThemeFilters.getSearchParameters(filters)
|
||||
|
||||
val url = "$apiUrl/book_search".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("query", query)
|
||||
.addIfNotBlank("genre[]", params.genre)
|
||||
.addIfNotBlank("status", params.status)
|
||||
.addIfNotBlank("searchMethod", params.searchMethod)
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.apply {
|
||||
params.categories.forEach {
|
||||
addQueryParameter("categories[]", it)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.replace("/book/", "/manga/")
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(apiUrl + manga.url, headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val info = response.parseAs<MangaInfoDto>()
|
||||
thumbnail_url = info.thumbnail_url
|
||||
title = info.name
|
||||
description = info.description
|
||||
genre = info.categories.joinToString()
|
||||
url = "/book/${info.id}"
|
||||
status = when (info.status) {
|
||||
1 -> SManga.ONGOING
|
||||
2 -> SManga.COMPLETED
|
||||
3, 4 -> SManga.CANCELLED
|
||||
5 -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$apiUrl/book_cap_units_all?manga_id=${manga.url.substringAfterLast("/")}", headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val items = response.parseAs<List<ChapterDto>>()
|
||||
val mangaId = response.request.url.queryParameter("manga_id")!!
|
||||
return items.map {
|
||||
SChapter.create().apply {
|
||||
name = "Cap " + parseChapterNumber(it.number)
|
||||
date_upload = parseChapterDate(it.updated_at)
|
||||
chapter_number = it.number
|
||||
scanlator = it.scan?.scan_name
|
||||
url = "/book_cap_units?manga_id=$mangaId&cap=${it.number}"
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
private fun parseChapterNumber(number: Float): String {
|
||||
val cap = number + 1F
|
||||
return "%.2f".format(cap)
|
||||
.let { if (cap < 10F) "0$it" else it }
|
||||
.replace(",00", "")
|
||||
.replace(",", ".")
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { 0L }
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
val url = "$baseUrl${chapter.url}".toHttpUrl()
|
||||
val id = url.queryParameter("manga_id")!!
|
||||
val cap = url.queryParameter("cap")!!.toFloat()
|
||||
val num = parseChapterNumber(cap)
|
||||
return "$baseUrl/ler/$id/cap-$num"
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
override fun pageListRequest(chapter: SChapter) = GET(apiUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body.string()
|
||||
val pages = if (body.startsWith("{")) {
|
||||
json.decodeFromString<Map<String, PageListDto>>(body).values.flatMap { it.pages }
|
||||
} else {
|
||||
json.decodeFromString<List<PageListDto>>(body).flatMap { it.pages }
|
||||
}
|
||||
|
||||
return pages.mapIndexed { index, item ->
|
||||
Page(index, "", item.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
/**
|
||||
* Handles a large manga list and returns a paginated response.
|
||||
* The app can't handle the large JSON list without pagination.
|
||||
*
|
||||
* @param page The page number to retrieve.
|
||||
* @param cache The cached manga page containing the full list of mangas.
|
||||
*/
|
||||
private fun pageableOf(page: Int, cache: MangasPage) = Observable.just(cache).map { mangaPage ->
|
||||
val mangas = mangaPage.mangas
|
||||
val pageSize = 15
|
||||
|
||||
val currentSlice = (page - 1) * pageSize
|
||||
|
||||
val startIndex = min(mangas.size, currentSlice)
|
||||
val endIndex = min(mangas.size, currentSlice + pageSize)
|
||||
|
||||
val slice = mangas.subList(startIndex, endIndex)
|
||||
|
||||
MangasPage(slice, hasNextPage = endIndex < mangas.size)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream(it.body.byteStream())
|
||||
}
|
||||
|
||||
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
|
||||
if (value.isNotBlank()) addQueryParameter(query, value)
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.slimeread
|
||||
package eu.kanade.tachiyomi.multisrc.slimereadtheme
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
object SlimeReadFilters {
|
||||
object SlimeReadThemeFilters {
|
||||
open class SelectFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.slimeread
|
||||
package eu.kanade.tachiyomi.multisrc.slimereadtheme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -11,7 +11,7 @@ import kotlin.system.exitProcess
|
|||
* Springboard that accepts https://slimeread.com/manga/<id>/<slug> intents
|
||||
* and redirects them to the main Tachiyomi process.
|
||||
*/
|
||||
class SlimeReadUrlActivity : Activity() {
|
||||
class SlimeReadThemeUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
|
@ -22,7 +22,7 @@ class SlimeReadUrlActivity : Activity() {
|
|||
val item = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${SlimeRead.PREFIX_SEARCH}$item")
|
||||
putExtra("query", "${SlimeReadTheme.PREFIX_SEARCH}$item")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ data class MangaInfoDto(
|
|||
@Serializable
|
||||
data class ChapterDto(
|
||||
@SerialName("btc_cap") val number: Float,
|
||||
@SerialName("btc_date_updated") val updated_at: String,
|
||||
val scan: ScanDto?,
|
||||
) {
|
||||
@Serializable
|
|
@ -118,7 +118,8 @@ abstract class VerComics(
|
|||
protected open val pageListSelector =
|
||||
"div.wp-content p > img:not(noscript img), " +
|
||||
"div.wp-content div#lector > img:not(noscript img), " +
|
||||
"div.wp-content > figure img:not(noscript img)"
|
||||
"div.wp-content > figure img:not(noscript img)," +
|
||||
"div.wp-content > img, div.wp-content > p img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector)
|
||||
.mapIndexed { i, img -> Page(i, imageUrl = img.imgAttr()) }
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
||||
baseVersionCode = 10
|
||||
|
|
|
@ -27,6 +27,8 @@ abstract class ZeistManga(
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
protected val json: Json by injectLazy()
|
||||
|
||||
private val intl by lazy { ZeistMangaIntl(lang) }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 43
|
||||
extVersionCode = 46
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ import kotlinx.serialization.json.jsonObject
|
|||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
|
@ -42,7 +41,6 @@ import java.text.ParseException
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class BatoTo(
|
||||
final override val lang: String,
|
||||
|
@ -51,10 +49,11 @@ open class BatoTo(
|
|||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.migrateMirrorPref()
|
||||
}
|
||||
|
||||
override val name: String = "Bato.to"
|
||||
override val baseUrl: String by lazy { getMirrorPref()!! }
|
||||
override val baseUrl: String get() = mirror
|
||||
override val id: Long = when (lang) {
|
||||
"zh-Hans" -> 2818874445640189582
|
||||
"zh-Hant" -> 38886079663327225
|
||||
|
@ -70,12 +69,9 @@ open class BatoTo(
|
|||
entryValues = MIRROR_PREF_ENTRY_VALUES
|
||||
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit()
|
||||
mirror = newValue as String
|
||||
true
|
||||
}
|
||||
}
|
||||
val altChapterListPref = CheckBoxPreference(screen.context).apply {
|
||||
|
@ -83,11 +79,6 @@ open class BatoTo(
|
|||
title = ALT_CHAPTER_LIST_PREF_TITLE
|
||||
summary = ALT_CHAPTER_LIST_PREF_SUMMARY
|
||||
setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
val removeOfficialPref = CheckBoxPreference(screen.context).apply {
|
||||
key = "${REMOVE_TITLE_VERSION_PREF}_$lang"
|
||||
|
@ -103,18 +94,35 @@ open class BatoTo(
|
|||
screen.addPreference(removeOfficialPref)
|
||||
}
|
||||
|
||||
private var mirror = ""
|
||||
get() {
|
||||
val current = field
|
||||
if (current.isNotEmpty()) {
|
||||
return current
|
||||
}
|
||||
field = getMirrorPref()!!
|
||||
return field
|
||||
}
|
||||
|
||||
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||
private fun isRemoveTitleVersion(): Boolean {
|
||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
|
||||
val selectedMirror = getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
if (selectedMirror in DEPRECATED_MIRRORS) {
|
||||
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
private val json: Json by injectLazy()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page", headers)
|
||||
|
@ -324,17 +332,28 @@ open class BatoTo(
|
|||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
private var titleRegex: Regex =
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)")
|
||||
Regex("\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|\\/Official|\\/ Official", RegexOption.IGNORE_CASE)
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
|
||||
val manga = SManga.create()
|
||||
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
||||
val alternativeTitles = document.select("div.pb-2.alias-set.line-b-f").text()
|
||||
val description = infoElement.select("div.limit-html").text() + "\n" +
|
||||
infoElement.select(".episode-list > .alert-warning").text().trim()
|
||||
val description = buildString {
|
||||
append(infoElement.select("div.limit-html").text())
|
||||
infoElement.selectFirst(".episode-list > .alert-warning")?.also {
|
||||
append("\n\n${it.text()}")
|
||||
}
|
||||
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
|
||||
append("\n\nExtra Info:\n${it.text()}")
|
||||
}
|
||||
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
|
||||
append("\n\nAlternative Titles:\n")
|
||||
append(it.text().split('/').joinToString("\n") { "• ${it.trim()}" })
|
||||
}
|
||||
}
|
||||
|
||||
val cleanedTitle = if (isRemoveTitleVersion()) {
|
||||
originalTitle.replace(titleRegex, "").trim()
|
||||
} else {
|
||||
|
@ -346,8 +365,7 @@ open class BatoTo(
|
|||
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
|
||||
manga.status = parseStatus(workStatus, uploadStatus)
|
||||
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
||||
manga.description = description +
|
||||
if (alternativeTitles.isNotBlank()) "\n\nAlternative Titles:\n$alternativeTitles" else ""
|
||||
manga.description = description
|
||||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
||||
return manga
|
||||
}
|
||||
|
@ -983,7 +1001,7 @@ open class BatoTo(
|
|||
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||
"bato.to",
|
||||
"zbato.org",
|
||||
"batocomic.com",
|
||||
"batocomic.net",
|
||||
"batocomic.org",
|
||||
|
@ -992,9 +1010,6 @@ open class BatoTo(
|
|||
"battwo.com",
|
||||
"comiko.net",
|
||||
"comiko.org",
|
||||
"mangatoto.com",
|
||||
"mangatoto.net",
|
||||
"mangatoto.org",
|
||||
"readtoto.com",
|
||||
"readtoto.net",
|
||||
"readtoto.org",
|
||||
|
@ -1009,11 +1024,17 @@ open class BatoTo(
|
|||
"xbato.org",
|
||||
"zbato.com",
|
||||
"zbato.net",
|
||||
"zbato.org",
|
||||
)
|
||||
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
|
||||
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
||||
|
||||
private val DEPRECATED_MIRRORS = listOf(
|
||||
"https://bato.to",
|
||||
"https://mangatoto.com",
|
||||
"https://mangatoto.net",
|
||||
"https://mangatoto.org",
|
||||
)
|
||||
|
||||
private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST"
|
||||
private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List"
|
||||
private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 50
|
||||
extVersionCode = 51
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ abstract class Comick(
|
|||
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
|
||||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addNetworkInterceptor(::errorInterceptor)
|
||||
.rateLimit(3, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Cubari'
|
||||
extClass = '.CubariFactory'
|
||||
extVersionCode = 24
|
||||
extVersionCode = 25
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -38,7 +38,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val headers = request.headers.newBuilder()
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.deviantart.DeviantArtUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
||||
<data android:host="www.deviantart.com"/>
|
||||
<data android:host="deviantart.com"/>
|
||||
|
||||
<data android:pathPattern="/..*/gallery/..*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,6 +1,6 @@
|
|||
ext {
|
||||
extName = 'NETCOMICS'
|
||||
extClass = '.NetcomicsFactory'
|
||||
extName = 'DeviantArt'
|
||||
extClass = '.DeviantArt'
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,173 @@
|
|||
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.parser.Parser
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class DeviantArt : HttpSource() {
|
||||
override val name = "DeviantArt"
|
||||
override val baseUrl = "https://deviantart.com"
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
|
||||
}
|
||||
|
||||
private val backendBaseUrl = "https://backend.deviantart.com"
|
||||
private fun backendBuilder() = backendBaseUrl.toHttpUrl().newBuilder()
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String?): Long {
|
||||
return try {
|
||||
dateFormat.parse(dateStr ?: "")!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val matchGroups = requireNotNull(
|
||||
Regex("""gallery:([\w-]+)(?:/(\d+))?""").matchEntire(query)?.groupValues,
|
||||
) { SEARCH_FORMAT_MSG }
|
||||
val username = matchGroups[1]
|
||||
val folderId = matchGroups[2].ifEmpty { "all" }
|
||||
return GET("$baseUrl/$username/gallery/$folderId", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val manga = mangaDetailsParse(response)
|
||||
return MangasPage(listOf(manga), false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
val subFolderGallery = document.selectFirst("#sub-folder-gallery")
|
||||
val manga = SManga.create().apply {
|
||||
// If manga is sub-gallery then use sub-gallery name, else use gallery name
|
||||
title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
||||
?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
|
||||
author = document.title().substringBefore(" ")
|
||||
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
|
||||
thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
|
||||
}
|
||||
manga.setUrlWithoutDomain(response.request.url.toString())
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
|
||||
val username = pathSegments[0]
|
||||
val folderId = pathSegments[2]
|
||||
|
||||
val query = if (folderId == "all") {
|
||||
"gallery:$username"
|
||||
} else {
|
||||
"gallery:$username/$folderId"
|
||||
}
|
||||
|
||||
val url = backendBuilder()
|
||||
.addPathSegment("rss.xml")
|
||||
.addQueryParameter("q", query)
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoupXml()
|
||||
val chapterList = parseToChapterList(document).toMutableList()
|
||||
var nextUrl = document.selectFirst("[rel=next]")?.absUrl("href")
|
||||
|
||||
while (nextUrl != null) {
|
||||
val newRequest = GET(nextUrl, headers)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
val newDocument = newResponse.asJsoupXml()
|
||||
val newChapterList = parseToChapterList(newDocument)
|
||||
chapterList.addAll(newChapterList)
|
||||
|
||||
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
||||
}
|
||||
|
||||
return indexChapterList(chapterList.toList())
|
||||
}
|
||||
|
||||
private fun parseToChapterList(document: Document): List<SChapter> {
|
||||
val items = document.select("item")
|
||||
return items.map {
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||
chapter.apply {
|
||||
name = it.selectFirst("title")!!.text()
|
||||
date_upload = parseDate(it.selectFirst("pubDate")?.text())
|
||||
scanlator = it.selectFirst("media|credit")?.text()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
|
||||
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
||||
// primitively index the list by checking the first and last dates
|
||||
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
|
||||
}
|
||||
} else {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = i.toFloat() + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
||||
return listOf(Page(0, imageUrl = imageUrl))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun Response.asJsoupXml(): Document {
|
||||
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.snowmtl
|
||||
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -7,28 +7,28 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class SnowmtlUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
class DeviantArtUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val item = pathSegments[1]
|
||||
|
||||
if (pathSegments != null && pathSegments.size >= 3) {
|
||||
val username = pathSegments[0]
|
||||
val folderId = pathSegments[2]
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${Snowmtl.PREFIX_SEARCH}$item")
|
||||
putExtra("query", "gallery:$username/$folderId")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(tag, e.toString())
|
||||
Log.e("DeviantArtUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e(tag, "could not parse uri from intent $intent")
|
||||
Log.e("DeviantArtUrlActivity", "Could not parse URI from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'E-Hentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 24
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -48,11 +48,13 @@ abstract class EHentai(
|
|||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||
private val memberId: String by lazy { getMemberIdPref() }
|
||||
private val passHash: String by lazy { getPassHashPref() }
|
||||
private val igneous: String by lazy { getIgneousPref() }
|
||||
private val forceEh: Boolean by lazy { getForceEhPref() }
|
||||
|
||||
override val baseUrl: String
|
||||
get() = when {
|
||||
System.getenv("CI") == "true" -> "https://e-hentai.org"
|
||||
memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
|
||||
!forceEh && memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
|
||||
else -> "https://e-hentai.org"
|
||||
}
|
||||
|
||||
|
@ -170,18 +172,18 @@ abstract class EHentai(
|
|||
query.isBlank() -> languageTag(enforceLanguageFilter)
|
||||
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
|
||||
}
|
||||
filters.filterIsInstance<TextFilter>().forEach { it ->
|
||||
if (it.state.isNotEmpty()) {
|
||||
val splitted = it.state.split(",").filter(String::isNotBlank)
|
||||
if (splitted.size < 2 && it.type != "tags") {
|
||||
modifiedQuery += " ${it.type}:\"${it.state.replace(" ", "+")}\""
|
||||
filters.filterIsInstance<TextFilter>().forEach { filter ->
|
||||
if (filter.state.isNotEmpty()) {
|
||||
val splitted = filter.state.split(",").filter(String::isNotBlank)
|
||||
if (splitted.size < 2 && filter.type != "tags") {
|
||||
modifiedQuery += " ${filter.type}:\"${filter.state.replace(" ", "+")}\""
|
||||
} else {
|
||||
splitted.forEach { tag ->
|
||||
val trimmed = tag.trim().lowercase()
|
||||
if (trimmed.startsWith('-')) {
|
||||
modifiedQuery += " -${it.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
|
||||
modifiedQuery += if (trimmed.startsWith('-')) {
|
||||
" -${filter.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
|
||||
} else {
|
||||
modifiedQuery += " ${it.type}:\"${trimmed.replace(" ", "+")}\""
|
||||
" ${filter.type}:\"${trimmed.replace(" ", "+")}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -378,7 +380,7 @@ abstract class EHentai(
|
|||
|
||||
cookies["ipb_pass_hash"] = passHash
|
||||
|
||||
cookies["igneous"] = ""
|
||||
cookies["igneous"] = igneous
|
||||
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
@ -398,7 +400,7 @@ abstract class EHentai(
|
|||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.addInterceptor { chain ->
|
||||
val newReq = chain
|
||||
|
@ -414,6 +416,7 @@ abstract class EHentai(
|
|||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
EnforceLanguageFilter(getEnforceLanguagePref()),
|
||||
Favorites(),
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
|
@ -435,6 +438,14 @@ abstract class EHentai(
|
|||
}
|
||||
}
|
||||
|
||||
class Favorites : CheckBox("Favorites"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendPath("favorites.php")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenreOption(name: String, private val genreId: String) : CheckBox(name, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
|
||||
|
@ -561,21 +572,33 @@ abstract class EHentai(
|
|||
private const val PASS_HASH_PREF_TITLE = "ipb_pass_hash"
|
||||
private const val PASS_HASH_PREF_SUMMARY = "ipb_pass_hash value"
|
||||
private const val PASS_HASH_PREF_DEFAULT_VALUE = ""
|
||||
|
||||
private const val IGNEOUS_PREF_KEY = "IGNEOUS"
|
||||
private const val IGNEOUS_PREF_TITLE = "igneous"
|
||||
private const val IGNEOUS_PREF_SUMMARY = "igneous value override"
|
||||
private const val IGNEOUS_PREF_DEFAULT_VALUE = ""
|
||||
|
||||
private const val FORCE_EH = "FORCE_EH"
|
||||
private const val FORCE_EH_TITLE = "Force e-hentai"
|
||||
private const val FORCE_EH_SUMMARY = "Force e-hentai to avoid content on exhentai"
|
||||
private const val FORCE_EH_DEFAULT_VALUE = true
|
||||
}
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val forceEhPref = CheckBoxPreference(screen.context).apply {
|
||||
key = FORCE_EH
|
||||
title = FORCE_EH_TITLE
|
||||
summary = FORCE_EH_SUMMARY
|
||||
setDefaultValue(FORCE_EH_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
val enforceLanguagePref = CheckBoxPreference(screen.context).apply {
|
||||
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
|
||||
title = ENFORCE_LANGUAGE_PREF_TITLE
|
||||
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
|
||||
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
|
||||
val memberIdPref = EditTextPreference(screen.context).apply {
|
||||
|
@ -593,8 +616,19 @@ abstract class EHentai(
|
|||
|
||||
setDefaultValue(PASS_HASH_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
val igneousPref = EditTextPreference(screen.context).apply {
|
||||
key = IGNEOUS_PREF_KEY
|
||||
title = IGNEOUS_PREF_TITLE
|
||||
summary = IGNEOUS_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(IGNEOUS_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
screen.addPreference(forceEhPref)
|
||||
screen.addPreference(memberIdPref)
|
||||
screen.addPreference(passHashPref)
|
||||
screen.addPreference(igneousPref)
|
||||
screen.addPreference(enforceLanguagePref)
|
||||
}
|
||||
|
||||
|
@ -629,4 +663,12 @@ abstract class EHentai(
|
|||
private fun getMemberIdPref(): String {
|
||||
return getCookieValue(MEMBER_ID_PREF_TITLE, MEMBER_ID_PREF_DEFAULT_VALUE, MEMBER_ID_PREF_KEY)
|
||||
}
|
||||
|
||||
private fun getIgneousPref(): String {
|
||||
return getCookieValue(IGNEOUS_PREF_TITLE, IGNEOUS_PREF_DEFAULT_VALUE, IGNEOUS_PREF_KEY)
|
||||
}
|
||||
|
||||
private fun getForceEhPref(): Boolean {
|
||||
return preferences.getBoolean(FORCE_EH, FORCE_EH_DEFAULT_VALUE)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.EternalMangasFactory'
|
||||
themePkg = 'mangaesp'
|
||||
baseUrl = 'https://eternalmangas.com'
|
||||
overrideVersionCode = 1
|
||||
overrideVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.TopSeriesDto
|
||||
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.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okhttp3.FormBody
|
||||
|
@ -27,21 +24,7 @@ open class EternalMangas(
|
|||
"https://eternalmangas.com",
|
||||
lang,
|
||||
) {
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val body = response.body.string()
|
||||
val responseData = json.decodeFromString<TopSeriesDto>(body)
|
||||
|
||||
val topDaily = responseData.response.topDaily.flatten().map { it.data }
|
||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.filter { it.language == internalLang }
|
||||
.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
override val useApiSearch = true
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||
|
@ -49,16 +32,8 @@ open class EternalMangas(
|
|||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson)
|
||||
.filter { it.language == internalLang }
|
||||
.toMutableList()
|
||||
return parseComicsList(page, query, filters)
|
||||
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
||||
return this.filter { it.language == internalLang }.toMutableList()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaifox
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.SortOrderFilter
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.toDate
|
||||
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.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class HentaiFox(
|
||||
|
@ -100,4 +112,103 @@ class HentaiFox(
|
|||
Filter.Header("HINT: Use double quote (\") for exact match"),
|
||||
) + super.getFilterList().list,
|
||||
)
|
||||
|
||||
private val sidebarPath = "includes/sidebar.php"
|
||||
|
||||
private fun sidebarMangaSelector() = "div.item"
|
||||
|
||||
private fun Element.sidebarMangaTitle() =
|
||||
selectFirst("img")?.attr("alt")
|
||||
|
||||
private fun Element.sidebarMangaUrl() =
|
||||
selectFirst("a")?.attr("abs:href")
|
||||
|
||||
private fun Element.sidebarMangaThumbnail() =
|
||||
selectFirst("img")?.imgAttr()
|
||||
|
||||
private var csrfToken: String? = null
|
||||
|
||||
override fun tagsParser(document: Document): List<Genre> {
|
||||
csrfToken = csrfParser(document)
|
||||
return super.tagsParser(document)
|
||||
}
|
||||
|
||||
private fun csrfParser(document: Document): String {
|
||||
return document.select("[name=csrf-token]").attr("content")
|
||||
}
|
||||
|
||||
private fun setSidebarHeaders(csrfToken: String?): Headers {
|
||||
if (csrfToken == null) {
|
||||
return xhrHeaders
|
||||
}
|
||||
return xhrHeaders.newBuilder()
|
||||
.add("X-Csrf-Token", csrfToken)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Sidebar mangas should always override any other search, so they should appear first
|
||||
// and only propagate to super when a "normal" search is issued
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
|
||||
sortOrderFilter?.let {
|
||||
val selectedCategory = sortOrderFilter.values.get(sortOrderFilter.state)
|
||||
if (sidebarCategoriesFilterStateMap.containsKey(selectedCategory)) {
|
||||
return sidebarRequest(
|
||||
sidebarCategoriesFilterStateMap.getValue(selectedCategory),
|
||||
)
|
||||
}
|
||||
}
|
||||
return super.searchMangaRequest(page, query, filters)
|
||||
}
|
||||
|
||||
private fun sidebarRequest(category: String): Request {
|
||||
val url = "$baseUrl/$sidebarPath"
|
||||
return POST(
|
||||
url,
|
||||
setSidebarHeaders(csrfToken),
|
||||
FormBody.Builder()
|
||||
.add("type", category)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.encodedPath.endsWith(sidebarPath)) {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(sidebarMangaSelector())
|
||||
.map {
|
||||
SMangaDto(
|
||||
title = it.sidebarMangaTitle()!!,
|
||||
url = it.sidebarMangaUrl()!!,
|
||||
thumbnail = it.sidebarMangaThumbnail(),
|
||||
lang = LANGUAGE_MULTI,
|
||||
)
|
||||
}
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
setUrlWithoutDomain(it.url)
|
||||
thumbnail_url = it.thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
} else {
|
||||
return super.searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSortOrderURIs(): List<Pair<String, String>> {
|
||||
return super.getSortOrderURIs() + sidebarCategoriesFilterStateMap.toList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val sidebarCategoriesFilterStateMap = mapOf(
|
||||
"Top Rated" to "top_rated",
|
||||
"Most Faved" to "top_faved",
|
||||
"Most Fapped" to "top_fapped",
|
||||
"Most Downloaded" to "top_downloaded",
|
||||
).withDefault { "top_rated" }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hitomi'
|
||||
extClass = '.HitomiFactory'
|
||||
extVersionCode = 33
|
||||
extVersionCode = 35
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.all.hitomi
|
|||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
|
@ -29,6 +30,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -42,6 +44,7 @@ import java.util.LinkedList
|
|||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
class Hitomi(
|
||||
|
@ -62,7 +65,10 @@ class Hitomi(
|
|||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(::Intercept)
|
||||
.addInterceptor(::jxlContentTypeInterceptor)
|
||||
.apply {
|
||||
interceptors().add(0, ::streamResetRetry)
|
||||
}
|
||||
.build()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
|
@ -708,7 +714,7 @@ class Hitomi(
|
|||
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
|
||||
}
|
||||
|
||||
private fun Intercept(chain: Interceptor.Chain): Response {
|
||||
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.headers["Content-Type"] != "application/octet-stream") {
|
||||
return response
|
||||
|
@ -728,6 +734,20 @@ class Hitomi(
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun streamResetRetry(chain: Interceptor.Chain): Response {
|
||||
return try {
|
||||
chain.proceed(chain.request())
|
||||
} catch (e: StreamResetException) {
|
||||
Log.e(name, "reset", e)
|
||||
if (e.message.orEmpty().contains("INTERNAL_ERROR")) {
|
||||
Thread.sleep(2.seconds.inWholeMilliseconds)
|
||||
chain.proceed(chain.request())
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
|
|
@ -22,9 +22,9 @@ class Gallery(
|
|||
@Serializable
|
||||
class ImageFile(
|
||||
val hash: String,
|
||||
val haswebp: Int,
|
||||
val hasavif: Int,
|
||||
val hasjxl: Int,
|
||||
val haswebp: Int?,
|
||||
val hasavif: Int?,
|
||||
val hasjxl: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Komga'
|
||||
extClass = '.KomgaFactory'
|
||||
extVersionCode = 58
|
||||
extVersionCode = 59
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -156,6 +156,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
1 -> if (type == "series") "metadata.titleSort" else "name"
|
||||
2 -> "createdDate"
|
||||
3 -> "lastModifiedDate"
|
||||
4 -> "random"
|
||||
else -> return@forEach
|
||||
} + "," + if (state.ascending) "asc" else "desc"
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ internal class TypeSelect : Filter.Select<String>(
|
|||
|
||||
internal class SeriesSort(selection: Selection? = null) : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated"),
|
||||
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated", "Random"),
|
||||
selection ?: Selection(0, false),
|
||||
)
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ open class MangaFire(
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ open class MangaReader(
|
|||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Meitua.top'
|
||||
extClass = '.MeituaTop'
|
||||
extVersionCode = 8
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class MeituaTop : HttpSource() {
|
|||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
override val baseUrl = "https://88188.meitu.lol"
|
||||
override val baseUrl = "https://7a.meitu1.mom"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Mitaku'
|
||||
extClass = '.Mitaku'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class Mitaku : ParsedHttpSource() {
|
|||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/ero-cosplay/page/$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.article-container article"
|
||||
override fun popularMangaSelector() = "div.cm-primary article"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MyReadingManga'
|
||||
extClass = '.MyReadingMangaFactory'
|
||||
extVersionCode = 53
|
||||
extVersionCode = 56
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
|
@ -29,10 +28,21 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
|||
// Basic Info
|
||||
override val name = "MyReadingManga"
|
||||
final override val baseUrl = "https://myreadingmanga.info"
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override fun headersBuilder(): Headers.Builder =
|
||||
super.headersBuilder()
|
||||
.set("User-Agent", USER_AGENT)
|
||||
.add("X-Requested-With", randomString((1..20).random()))
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val headers = request.headers.newBuilder().apply {
|
||||
removeAll("X-Requested-With")
|
||||
}.build()
|
||||
|
||||
chain.proceed(request.newBuilder().headers(headers).build())
|
||||
}
|
||||
.build()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Popular - Random
|
||||
|
@ -308,7 +318,16 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
|||
Filter.Select<String>(displayName, vals.map { it }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder, uriParam: String) {
|
||||
if (state != 0 || !firstIsUnspecified) {
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
|
||||
val splitFilter = vals[state].split(",")
|
||||
when {
|
||||
splitFilter.size == 2 -> {
|
||||
val reversedFilter = splitFilter.reversed().joinToString(" | ").trim()
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:$reversedFilter")
|
||||
}
|
||||
else -> {
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,6 +340,11 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36"
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
private fun randomString(length: Int): String {
|
||||
val charPool = ('a'..'z') + ('A'..'Z')
|
||||
return List(length) { charPool.random() }.joinToString("")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.namicomi.NamiComiUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter
|
||||
android:autoVerify="false"
|
||||
tools:targetApi="23">
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="namicomi.com" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:pathPattern="/.*/title/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,114 @@
|
|||
content=Content
|
||||
content_rating=Content rating
|
||||
content_rating_genre=Content rating: %s
|
||||
content_rating_mature=Mature
|
||||
content_rating_restricted=Restricted
|
||||
content_rating_safe=Safe
|
||||
content_warnings_drugs=Drugs
|
||||
content_warnings_gambling=Gambling
|
||||
content_warnings_gore=Gore
|
||||
content_warnings_mental_disorders=Mental Disorders
|
||||
content_warnings_physical_abuse=Physical Abuse
|
||||
content_warnings_racism=Racism
|
||||
content_warnings_self_harm=Self-harm
|
||||
content_warnings_sexual_abuse=Sexual Abuse
|
||||
content_warnings_verbal_abuse=Verbal Abuse
|
||||
cover_quality=Cover quality
|
||||
cover_quality_low=Low
|
||||
cover_quality_medium=Medium
|
||||
cover_quality_original=Original
|
||||
data_saver=Data saver
|
||||
data_saver_summary=Enables smaller, more compressed images
|
||||
error_payment_required=Payment required. Chapter requires a premium subscription
|
||||
excluded_tags_mode=Excluded tags mode
|
||||
format=Format
|
||||
format_4_koma=4-Koma
|
||||
format_adaptation=Adaptation
|
||||
format_anthology=Anthology
|
||||
format_full_color=Full Color
|
||||
format_oneshot=Oneshot
|
||||
format_silent=Silent
|
||||
genre=Genre
|
||||
genre_action=Action
|
||||
genre_adventure=Adventure
|
||||
genre_boys_love=Boys' Love
|
||||
genre_comedy=Comedy
|
||||
genre_crime=Crime
|
||||
genre_drama=Drama
|
||||
genre_fantasy=Fantasy
|
||||
genre_girls_love=Girls' Love
|
||||
genre_historical=Historical
|
||||
genre_horror=Horror
|
||||
genre_isekai=Isekai
|
||||
genre_mecha=Mecha
|
||||
genre_medical=Medical
|
||||
genre_mystery=Mystery
|
||||
genre_philosophical=Philosophical
|
||||
genre_psychological=Psychological
|
||||
genre_romance=Romance
|
||||
genre_sci_fi=Sci-Fi
|
||||
genre_slice_of_life=Slice of Life
|
||||
genre_sports=Sports
|
||||
genre_superhero=Superhero
|
||||
genre_thriller=Thriller
|
||||
genre_tragedy=Tragedy
|
||||
genre_wuxia=Wuxia
|
||||
has_available_chapters=Has available chapters
|
||||
included_tags_mode=Included tags mode
|
||||
invalid_manga_id=Not a valid title ID
|
||||
mode_and=And
|
||||
mode_or=Or
|
||||
show_locked_chapters=Show locked/paywalled chapters
|
||||
show_locked_chapters_summary=Display chapters that require an account with a premium subscription
|
||||
sort=Sort
|
||||
sort_alphabetic=Alphabetic
|
||||
sort_content_created_at=Content created at
|
||||
sort_number_of_chapters=Chapter count
|
||||
sort_number_of_comments=Comment count
|
||||
sort_number_of_follows=Followers
|
||||
sort_number_of_likes=Likes
|
||||
sort_rating=Rating
|
||||
sort_views=Views
|
||||
sort_year=Year
|
||||
status=Status
|
||||
status_cancelled=Cancelled
|
||||
status_completed=Completed
|
||||
status_hiatus=Hiatus
|
||||
status_ongoing=Ongoing
|
||||
tags_mode=Tags mode
|
||||
theme=Theme
|
||||
theme_aliens=Aliens
|
||||
theme_animals=Animals
|
||||
theme_cooking=Cooking
|
||||
theme_crossdressing=Crossdressing
|
||||
theme_delinquents=Delinquents
|
||||
theme_demons=Demons
|
||||
theme_genderswap=Genderswap
|
||||
theme_ghosts=Ghosts
|
||||
theme_gyaru=Gyaru
|
||||
theme_harem=Harem
|
||||
theme_mafia=Mafia
|
||||
theme_magic=Magic
|
||||
theme_magical_girls=Magical Girls
|
||||
theme_martial_arts=Martial Arts
|
||||
theme_military=Military
|
||||
theme_monster_girls=Monster Girls
|
||||
theme_monsters=Monsters
|
||||
theme_music=Music
|
||||
theme_ninja=Ninja
|
||||
theme_office_workers=Office Workers
|
||||
theme_police=Police
|
||||
theme_post_apocalyptic=Post-Apocalyptic
|
||||
theme_reincarnation=Reincarnation
|
||||
theme_reverse_harem=Reverse Harem
|
||||
theme_samurai=Samurai
|
||||
theme_school_life=School Life
|
||||
theme_supernatural=Supernatural
|
||||
theme_survival=Survival
|
||||
theme_time_travel=Time Travel
|
||||
theme_traditional_games=Traditional Games
|
||||
theme_vampires=Vampires
|
||||
theme_video_games=Video Games
|
||||
theme_villainess=Villainess
|
||||
theme_virtual_reality=Virtual Reality
|
||||
theme_zombies=Zombies
|
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'NamiComi'
|
||||
extClass = '.NamiComiFactory'
|
||||
extVersionCode = 2
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:i18n"))
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 11 KiB |