Compare commits
107 Commits
43bd7342e7
...
07eba2f8f2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
07eba2f8f2 | ||
![]() |
77bd833e6a | ||
![]() |
ea28acd641 | ||
![]() |
a8e99cea12 | ||
![]() |
e0193a707a | ||
![]() |
3fb70c10de | ||
![]() |
414b6b8670 | ||
![]() |
ee5a221a17 | ||
![]() |
d43dd231b2 | ||
![]() |
5b7ce2f85d | ||
![]() |
af0d261251 | ||
![]() |
7e13a5ca27 | ||
![]() |
3b85cfc5a2 | ||
![]() |
44a4f517d2 | ||
![]() |
63d0d76535 | ||
![]() |
72c0ecc64f | ||
![]() |
91b33530ac | ||
![]() |
f192dc994e | ||
![]() |
3bc05706e5 | ||
![]() |
3fe012a4c7 | ||
![]() |
f1731b643e | ||
![]() |
f692c85526 | ||
![]() |
3a8918ae58 | ||
![]() |
30a65efcb0 | ||
![]() |
c5fbede326 | ||
![]() |
6f47ed8bc1 | ||
![]() |
9dc2d6f03f | ||
![]() |
99f58ad3f1 | ||
![]() |
98f7d48324 | ||
![]() |
8a14edfd48 | ||
![]() |
99c8f52676 | ||
![]() |
2c52f117d4 | ||
![]() |
db2813a793 | ||
![]() |
74edbbae7d | ||
![]() |
456b3fff5e | ||
![]() |
de293f4685 | ||
![]() |
ac2ebae360 | ||
![]() |
55e3a5b101 | ||
![]() |
249ea8a3ad | ||
![]() |
d7c6574e87 | ||
![]() |
34b4284d7c | ||
![]() |
a4cd04699c | ||
![]() |
1c3e40c5dc | ||
![]() |
5508ced068 | ||
![]() |
f5aecd2ad4 | ||
![]() |
ddf63bf592 | ||
![]() |
7154880810 | ||
![]() |
851564a4e2 | ||
![]() |
31d83bbfc3 | ||
![]() |
4d5e44c2a6 | ||
![]() |
c986f2924c | ||
![]() |
d58a274c70 | ||
![]() |
577bbb69bf | ||
![]() |
38c5f53e3b | ||
![]() |
ab5474a92b | ||
![]() |
13949bc6f3 | ||
![]() |
d7f03a9caa | ||
![]() |
407af100d4 | ||
![]() |
57e51e8ef1 | ||
![]() |
ff9732e42b | ||
![]() |
5025203010 | ||
![]() |
7ad4d56426 | ||
![]() |
6205dad385 | ||
![]() |
fe96b31232 | ||
![]() |
21ee081352 | ||
![]() |
87e4096264 | ||
![]() |
0e3bf78d8d | ||
![]() |
8482b7d235 | ||
![]() |
2ed6687b26 | ||
![]() |
a20493a46c | ||
![]() |
08c73abe63 | ||
![]() |
0f1cbebf96 | ||
![]() |
cd03d78928 | ||
![]() |
ec62056e09 | ||
![]() |
5b8834fe6b | ||
![]() |
88ffec340b | ||
![]() |
03ee60f4e5 | ||
![]() |
789e2ca044 | ||
![]() |
6098ea8e35 | ||
![]() |
060473aedf | ||
![]() |
5aa7f1627c | ||
![]() |
a44693f757 | ||
![]() |
023942f27e | ||
![]() |
7b06cfa577 | ||
![]() |
d5acdde1d9 | ||
![]() |
5e4c156a27 | ||
![]() |
4fc0bc0efd | ||
![]() |
70c74d2b55 | ||
![]() |
75dad8e026 | ||
![]() |
cd28f381bf | ||
![]() |
7b52dd532e | ||
![]() |
d0004a3440 | ||
![]() |
308b9a09d3 | ||
![]() |
fca1ceeb89 | ||
![]() |
4defc529d3 | ||
![]() |
5767234606 | ||
![]() |
4446491912 | ||
![]() |
e2d5b6eb19 | ||
![]() |
20f78a35bb | ||
![]() |
481fd54452 | ||
![]() |
25a118d501 | ||
![]() |
ffd98958ee | ||
![]() |
e8fed7ce6d | ||
![]() |
de846ee0ad | ||
![]() |
98b871591e | ||
![]() |
7fbf5e8af6 | ||
![]() |
0239463aa8 |
@ -86,7 +86,7 @@ small, just do a normal full clone instead.**
|
||||
```bash
|
||||
git sparse-checkout set --cone --sparse-index
|
||||
# add project folders
|
||||
git sparse-checkout add buildSrc core gradle lib lib-multisrc
|
||||
git sparse-checkout add buildSrc core gradle lib lib-multisrc utils
|
||||
# add a single source
|
||||
git sparse-checkout add src/<lang>/<source>
|
||||
```
|
||||
|
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
2
gradlew
generated
vendored
2
gradlew
generated
vendored
@ -205,7 +205,7 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 7
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.bakkin
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@ -33,9 +33,7 @@ abstract class BakkinReaderX(
|
||||
"Android ${Build.VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${AppInfo.getVersionName()}"
|
||||
|
||||
protected val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
}
|
||||
protected val preferences by getPreferencesLazy()
|
||||
|
||||
private val json by lazy { Injekt.get<Json>() }
|
||||
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 7
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:synchrony"))
|
||||
|
@ -19,18 +19,23 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.tryParse
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -46,9 +51,7 @@ abstract class ColaManga(
|
||||
|
||||
private val intl = ColaMangaIntl(lang)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences by getPreferencesLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(
|
||||
@ -150,6 +153,8 @@ abstract class ColaManga(
|
||||
protected abstract val genreTitle: String
|
||||
protected abstract val statusOngoing: String
|
||||
protected abstract val statusCompleted: String
|
||||
protected abstract val lastUpdated: String
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h1.fed-part-eone")!!.text()
|
||||
@ -168,6 +173,15 @@ abstract class ColaManga(
|
||||
|
||||
override fun chapterListSelector(): String = "div:not(.fed-hidden) > div.all_data_list > ul.fed-part-rows a"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }.apply {
|
||||
if (isNotEmpty()) {
|
||||
this[0].date_upload = dateFormat.tryParse(document.selectFirst("span.fed-text-muted:contains($lastUpdated) + a")?.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
name = element.attr("title")
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 8
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:speedbinb"))
|
||||
|
@ -28,7 +28,7 @@ open class ComicGamma(
|
||||
|
||||
private val json = Injekt.get<Json>()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(SpeedBinbInterceptor(json))
|
||||
.build()
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
@ -39,7 +39,7 @@ abstract class FansubsCat(
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
|
||||
|
||||
override val client: OkHttpClient = network.client
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
||||
baseVersionCode = 10
|
||||
|
@ -19,6 +19,7 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import java.nio.charset.Charset
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
@ -246,7 +247,13 @@ abstract class FMReader(
|
||||
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
|
||||
}
|
||||
}
|
||||
date_upload = element.select(chapterTimeSelector).let { if (it.hasText()) parseRelativeDate(it.text()) else 0 }
|
||||
date_upload = element.select(chapterTimeSelector).let { dateElement ->
|
||||
if (dateElement.hasText()) {
|
||||
parseRelativeDate(dateElement.text()).takeIf { it != 0L } ?: parseAbsoluteDate(dateElement.text())
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,55 +264,63 @@ abstract class FMReader(
|
||||
open val dateWordIndex = 1
|
||||
|
||||
open fun parseRelativeDate(date: String): Long {
|
||||
val value = date.split(' ')[dateValueIndex].toInt()
|
||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||
if (it.contains("(")) {
|
||||
it.substringBefore("(")
|
||||
} else {
|
||||
it.substringBefore("s")
|
||||
try {
|
||||
val value = date.split(' ')[dateValueIndex].toInt()
|
||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||
if (it.contains("(")) {
|
||||
it.substringBefore("(")
|
||||
} else {
|
||||
it.substringBefore("s")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// languages: en, vi, es, tr
|
||||
return when (dateWord) {
|
||||
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value * 7)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
// languages: en, vi, es, tr
|
||||
return when (dateWord) {
|
||||
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value * 7)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
|
||||
open fun parseAbsoluteDate(dateStr: String): Long {
|
||||
return runCatching { dateFormat.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
return try {
|
||||
dateFormat.parse(dateStr)?.time ?: 0L
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
open val pageListImageSelector = "img.chapter-img"
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.foolslide
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -12,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@ -273,9 +273,7 @@ abstract class FoolSlide(
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
protected val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
}
|
||||
protected val preferences by getPreferencesLazy()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
CheckBoxPreference(screen.context).apply {
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 5
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -36,8 +36,6 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@ -56,9 +54,7 @@ abstract class GalleryAdults(
|
||||
.build()
|
||||
|
||||
/* Preferences */
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
protected open val useShortTitlePreference = true
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 30
|
||||
baseVersionCode = 31
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.grouple
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
@ -22,8 +22,6 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.text.DecimalFormat
|
||||
import java.text.ParseException
|
||||
@ -37,9 +35,7 @@ abstract class GroupLe(
|
||||
final override val lang: String,
|
||||
) : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.guya
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.preference.ListPreference
|
||||
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -25,8 +25,6 @@ import org.jsoup.Jsoup
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
abstract class Guya(
|
||||
override val name: String,
|
||||
@ -48,9 +46,7 @@ abstract class Guya(
|
||||
private val scanlators: ScanlatorStore = ScanlatorStore()
|
||||
|
||||
// Preferences configuration
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
// Request builder for the "browse" page of the manga
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
baseVersionCode = 29
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.heancms
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -26,8 +26,6 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.thread
|
||||
@ -39,9 +37,7 @@ abstract class HeanCms(
|
||||
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.hentaihand
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.text.InputType
|
||||
import android.widget.Toast
|
||||
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@ -32,8 +32,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
@ -318,9 +316,7 @@ abstract class HentaiHand(
|
||||
|
||||
// Preferences
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 8
|
||||
|
@ -93,21 +93,24 @@ class Chapter(
|
||||
private val id: Int,
|
||||
private val slug: String,
|
||||
private val number: JsonPrimitive,
|
||||
private val createdBy: Name,
|
||||
private val createdAt: String,
|
||||
private val chapterStatus: String,
|
||||
private val isAccessible: Boolean,
|
||||
private val isLocked: Boolean? = false,
|
||||
private val isTimeLocked: Boolean? = false,
|
||||
private val mangaPost: ChapterPostDetails,
|
||||
) {
|
||||
fun isPublic() = chapterStatus == "PUBLIC"
|
||||
|
||||
fun isAccessible() = isAccessible
|
||||
|
||||
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
|
||||
|
||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||
val prefix = if (isLocked()) "🔒 " else ""
|
||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
name = "Chapter $number"
|
||||
scanlator = createdBy.name
|
||||
name = "${prefix}Chapter $number"
|
||||
date_upload = try {
|
||||
dateFormat.parse(createdAt)!!.time
|
||||
} catch (_: ParseException) {
|
||||
|
@ -1,6 +1,10 @@
|
||||
package eu.kanade.tachiyomi.multisrc.iken
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
@ -9,32 +13,34 @@ 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 keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class Iken(
|
||||
override val name: String,
|
||||
override val lang: String,
|
||||
override val baseUrl: String,
|
||||
) : HttpSource() {
|
||||
val apiUrl: String = baseUrl,
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
private var genres = emptyList<Pair<String, String>>()
|
||||
protected val titleCache by lazy {
|
||||
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||
val response = client.newCall(GET("$apiUrl/api/query?perPage=9999", headers)).execute()
|
||||
val data = response.parseAs<SearchResponse>()
|
||||
|
||||
data.posts
|
||||
@ -65,7 +71,7 @@ abstract class Iken(
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
||||
val url = "$apiUrl/api/query".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("page", page.toString())
|
||||
addQueryParameter("perPage", perPage.toString())
|
||||
addQueryParameter("searchTerm", query.trim())
|
||||
@ -114,35 +120,76 @@ abstract class Iken(
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val id = manga.url.substringAfterLast("#")
|
||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
||||
|
||||
return GET(url, headers)
|
||||
return GET("$baseUrl/series/${manga.url}", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<Post<ChapterListResponse>>()
|
||||
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
|
||||
|
||||
val id = response.request.url.fragment!!
|
||||
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
|
||||
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
|
||||
|
||||
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
|
||||
|
||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||
|
||||
return data.post.chapters
|
||||
.filter { it.isPublic() && it.isAccessible() }
|
||||
.filter { it.isPublic() && (it.isAccessible() || (preferences.getBoolean(showLockedChapterPrefKey, false) && it.isLocked())) }
|
||||
.map { it.toSChapter(data.post.slug) }
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("main section img").mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img.absUrl("src"))
|
||||
if (document.selectFirst("svg.lucide-lock") != null) {
|
||||
throw Exception("Unlock chapter in webview")
|
||||
}
|
||||
|
||||
return document.getNextJson("images").parseAs<List<PageParseDto>>().mapIndexed { idx, p ->
|
||||
Page(idx, imageUrl = p.url)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageParseDto(
|
||||
val url: String,
|
||||
)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = showLockedChapterPrefKey
|
||||
title = "Show locked chapters"
|
||||
setDefaultValue(false)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString(body.string())
|
||||
protected fun Document.getNextJson(key: String): String {
|
||||
val data = selectFirst("script:containsData($key)")
|
||||
?.data()
|
||||
?: throw Exception("Unable to retrieve NEXT data")
|
||||
|
||||
val keyIndex = data.indexOf(key)
|
||||
val start = data.indexOf('[', keyIndex)
|
||||
|
||||
var depth = 1
|
||||
var i = start + 1
|
||||
|
||||
while (i < data.length && depth > 0) {
|
||||
when (data[i]) {
|
||||
'[' -> depth++
|
||||
']' -> depth--
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return "\"${data.substring(start, i)}\"".parseAs<String>()
|
||||
}
|
||||
}
|
||||
|
||||
private const val perPage = 18
|
||||
private const val showLockedChapterPrefKey = "pref_show_locked_chapters"
|
||||
private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""")
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 18
|
||||
baseVersionCode = 20
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.kemono
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -15,13 +14,12 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferences
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Thread.sleep
|
||||
import java.util.TimeZone
|
||||
@ -34,15 +32,14 @@ open class Kemono(
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.client.newBuilder().rateLimit(1).build()
|
||||
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
private val preferences = getPreferences()
|
||||
|
||||
private val apiPath = "api/v1"
|
||||
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 13
|
||||
baseVersionCode = 15
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.keyoapp
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -23,8 +23,6 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@ -37,9 +35,7 @@ abstract class Keyoapp(
|
||||
final override val lang: String,
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -63,7 +59,8 @@ abstract class Keyoapp(
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div.flex-col div.grid > div.group.border"
|
||||
override fun popularMangaSelector(): String =
|
||||
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.getImageUrl("*[style*=background-image]")
|
||||
@ -191,7 +188,7 @@ abstract class Keyoapp(
|
||||
}
|
||||
}
|
||||
|
||||
private fun genresRequest(): Request = GET("$baseUrl/series/", headers)
|
||||
protected open fun genresRequest(): Request = GET("$baseUrl/series/", headers)
|
||||
|
||||
/**
|
||||
* Get the genres from the search page document.
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
@ -111,7 +111,11 @@ abstract class LectorMoe(
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
|
||||
val seriesSlug = result.data.slug
|
||||
return result.data.chapters?.map { it.toSChapter(seriesSlug) } ?: emptyList()
|
||||
return result.data.chapters
|
||||
?.filter { it.subscribersOnly.not() }
|
||||
?.map { it.toSChapter(seriesSlug) }
|
||||
?.filter { it.date_upload < System.currentTimeMillis() }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
|
@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@Serializable
|
||||
class Data<T>(val data: T)
|
||||
@ -53,18 +54,21 @@ class SeriesAuthorDto(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SeriesChapterDto(
|
||||
private val title: String,
|
||||
private val number: Float,
|
||||
private val createdAt: String,
|
||||
private val releasedAt: String,
|
||||
val subscribersOnly: Boolean,
|
||||
) {
|
||||
fun toSChapter(seriesSlug: String) = SChapter.create().apply {
|
||||
name = "Capítulo ${number.toString().removeSuffix(".0")} - $title"
|
||||
date_upload = try {
|
||||
dateFormat.parse(createdAt)?.time ?: 0L
|
||||
dateFormat.parse(releasedAt)?.time ?: 0L
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 35
|
||||
baseVersionCode = 36
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.multisrc.libgroup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -57,9 +57,10 @@ abstract class LibGroup(
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.migrateOldImageServer()
|
||||
private val preferences by getPreferencesLazy {
|
||||
if (getString(SERVER_PREF, "main") == "fourth") {
|
||||
edit().putString(SERVER_PREF, "secondary").apply()
|
||||
}
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
@ -685,11 +686,4 @@ abstract class LibGroup(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// api changed id of servers, remap SERVER_PREF old("fourth") to new("secondary")
|
||||
private fun SharedPreferences.migrateOldImageServer(): SharedPreferences {
|
||||
if (getString(SERVER_PREF, "main") != "fourth") return this
|
||||
edit().putString(SERVER_PREF, "secondary").apply()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 5
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -30,8 +30,6 @@ import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
@ -51,9 +49,7 @@ abstract class MachineTranslations(
|
||||
|
||||
override val lang = language.lang
|
||||
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
/**
|
||||
* A flag that tracks whether the settings have been changed. It is used to indicate if
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 40
|
||||
baseVersionCode = 41
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
|
@ -160,7 +160,7 @@ abstract class Madara(
|
||||
}
|
||||
|
||||
// exclude/filter bilibili manga from list
|
||||
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector"
|
||||
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
|
||||
|
||||
open val popularMangaUrlSelector = "div.post-title a"
|
||||
|
||||
@ -584,7 +584,7 @@ abstract class Madara(
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.c-tabs-item__content"
|
||||
override fun searchMangaSelector() = "div.c-tabs-item__content , .manga__item"
|
||||
|
||||
protected open val searchMangaUrlSelector = "div.post-title a"
|
||||
|
||||
@ -754,7 +754,7 @@ abstract class Madara(
|
||||
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
|
||||
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
|
||||
open val mangaDetailsSelectorArtist = "div.artist-content > a"
|
||||
open val mangaDetailsSelectorStatus = "div.summary-content"
|
||||
open val mangaDetailsSelectorStatus = "div.summary-content, div.summary-heading:contains(Status) + div"
|
||||
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
|
||||
open val mangaDetailsSelectorThumbnail = "div.summary_image img"
|
||||
open val mangaDetailsSelectorGenre = "div.genres-content a"
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 17
|
||||
baseVersionCode = 18
|
||||
|
@ -41,6 +41,10 @@ abstract class MadTheme(
|
||||
.rateLimit(1, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
protected open val useLegacyApi = false
|
||||
|
||||
protected open val useSlugSearch = false
|
||||
|
||||
// TODO: better cookie sharing
|
||||
// TODO: don't count cached responses against rate limit
|
||||
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
@ -177,59 +181,57 @@ abstract class MadTheme(
|
||||
|
||||
val document = response.asJsoup()
|
||||
|
||||
// Need the total chapters to check against the request
|
||||
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
|
||||
?.substringAfter("(")
|
||||
?.substringBefore(")")
|
||||
?.toIntOrNull()
|
||||
|
||||
val script = document.selectFirst("script:containsData(bookId)")
|
||||
?: throw Exception("Cannot find script")
|
||||
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
|
||||
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
|
||||
|
||||
// Use slug search by default
|
||||
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
|
||||
if (!slugRequest.isSuccessful) {
|
||||
throw Exception("HTTP error ${slugRequest.code}")
|
||||
var chaptersList = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
val fetchApi = document.selectFirst("div#show-more-chapters > span")
|
||||
?.attr("onclick")?.equals("getChapters()")
|
||||
?: false
|
||||
|
||||
if (fetchApi) {
|
||||
val apiChapters = client.newCall(GET(buildChapterUrl(bookId, bookSlug), headers)).execute()
|
||||
.asJsoup().select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
val cutIndex = chaptersList.indexOfFirst { chapter ->
|
||||
apiChapters.any { it.url == chapter.url }
|
||||
}.takeIf { it != -1 } ?: chaptersList.size
|
||||
|
||||
chaptersList = (chaptersList.subList(0, cutIndex) + apiChapters)
|
||||
}
|
||||
|
||||
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
|
||||
|
||||
if (totalChapters != null && finalDocument.size < totalChapters) {
|
||||
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
|
||||
finalDocument = idRequest.asJsoup().select(chapterListSelector())
|
||||
}
|
||||
|
||||
return finalDocument.map {
|
||||
SChapter.create().apply {
|
||||
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
|
||||
name = it.selectFirst(".chapter-title")!!.text()
|
||||
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
|
||||
}
|
||||
}
|
||||
return chaptersList
|
||||
}
|
||||
|
||||
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
|
||||
private fun buildChapterUrl(mangaId: String, mangaSlug: String): HttpUrl {
|
||||
return baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addPathSegment("manga")
|
||||
addPathSegment(fetchByParam)
|
||||
addPathSegment(if (useSlugSearch) mangaSlug else mangaId)
|
||||
addPathSegment("chapters")
|
||||
addQueryParameter("source", "detail")
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
|
||||
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("manga_id", mangaId)
|
||||
.addQueryParameter("manga_name", manga.title)
|
||||
.fragment("idFound")
|
||||
.build()
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
if (useLegacyApi) {
|
||||
val mangaId = MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)
|
||||
val url = mangaId?.let {
|
||||
"$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("manga_id", it)
|
||||
.addQueryParameter("manga_name", manga.title)
|
||||
.fragment("idFound")
|
||||
.build()
|
||||
.toString()
|
||||
} ?: (baseUrl + manga.url)
|
||||
|
||||
GET(url, headers)
|
||||
} ?: GET("$baseUrl${manga.url}", headers)
|
||||
return GET(url, headers)
|
||||
}
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (genresList == null) {
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
@ -1,7 +1,11 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@ -9,42 +13,144 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.TimeZone
|
||||
import java.util.regex.Pattern
|
||||
|
||||
// Based off of Mangakakalot 1.2.8
|
||||
abstract class MangaBox(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
private val mirrorEntries: Array<String>,
|
||||
override val lang: String,
|
||||
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH),
|
||||
) : ParsedHttpSource() {
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat(
|
||||
"MMM-dd-yyyy HH:mm",
|
||||
Locale.ENGLISH,
|
||||
).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
},
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val baseUrl: String get() = mirror
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(::useAltCdnInterceptor)
|
||||
.build()
|
||||
|
||||
private fun SharedPreferences.getMirrorPref(): String =
|
||||
getString(PREF_USE_MIRROR, mirrorEntries[0])!!
|
||||
|
||||
private val preferences: SharedPreferences by getPreferencesLazy {
|
||||
// if current mirror is not in mirrorEntries, set default
|
||||
if (getMirrorPref() !in mirrorEntries.map { "${URL_PREFIX}$it" }) {
|
||||
edit().putString(PREF_USE_MIRROR, "${URL_PREFIX}${mirrorEntries[0]}").apply()
|
||||
}
|
||||
}
|
||||
|
||||
private var mirror = ""
|
||||
get() {
|
||||
if (field.isNotEmpty()) {
|
||||
return field
|
||||
}
|
||||
|
||||
field = preferences.getMirrorPref()
|
||||
return field
|
||||
}
|
||||
|
||||
private val cdnSet =
|
||||
MangaBoxLinkedCdnSet() // Stores all unique CDNs that the extension can use to retrieve chapter images
|
||||
|
||||
private class MangaBoxFallBackTag // Custom empty class tag to use as an identifier that the specific request is fallback-able
|
||||
|
||||
private fun HttpUrl.getBaseUrl(): String =
|
||||
"${URL_PREFIX}${this.host}${
|
||||
when (this.port) {
|
||||
80, 443 -> ""
|
||||
else -> ":${this.port}"
|
||||
}
|
||||
}"
|
||||
|
||||
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
|
||||
val originalResponse: Response? = try {
|
||||
chain.proceed(request)
|
||||
} catch (e: IOException) {
|
||||
if (requestTag == null) {
|
||||
throw e
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (requestTag == null || originalResponse?.isSuccessful == true) {
|
||||
requestTag?.let {
|
||||
// Move working cdn to first so it gets priority during iteration
|
||||
cdnSet.moveItemToFirst(request.url.getBaseUrl())
|
||||
}
|
||||
|
||||
return originalResponse!!
|
||||
}
|
||||
|
||||
// Close the original response if it's not successful
|
||||
originalResponse?.close()
|
||||
|
||||
for (cdnUrl in cdnSet) {
|
||||
var tryResponse: Response? = null
|
||||
|
||||
try {
|
||||
val newUrl = cdnUrl.toHttpUrl().newBuilder()
|
||||
.encodedPath(request.url.encodedPath)
|
||||
.fragment(request.url.fragment)
|
||||
.build()
|
||||
|
||||
// Create a new request with the updated URL
|
||||
val newRequest = request.newBuilder()
|
||||
.url(newUrl)
|
||||
.build()
|
||||
|
||||
// Proceed with the new request
|
||||
tryResponse = chain.proceed(newRequest)
|
||||
|
||||
// Check if the response is successful
|
||||
if (tryResponse.isSuccessful) {
|
||||
// Move working cdn to first so it gets priority during iteration
|
||||
cdnSet.moveItemToFirst(newRequest.url.getBaseUrl())
|
||||
|
||||
return tryResponse
|
||||
}
|
||||
|
||||
tryResponse.close()
|
||||
} catch (_: IOException) {
|
||||
tryResponse?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// If all CDNs fail, throw an error
|
||||
return throw IOException("All CDN attempts failed.")
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", baseUrl) // for covers
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
open val popularUrlPath = "manga_list?type=topview&category=all&state=all&page="
|
||||
open val popularUrlPath = "manga-list/hot-manga?page="
|
||||
|
||||
open val latestUrlPath = "manga_list?type=latest&category=all&state=all&page="
|
||||
open val latestUrlPath = "manga-list/latest-manga?page="
|
||||
|
||||
open val simpleQueryPath = "search/"
|
||||
open val simpleQueryPath = "search/story/"
|
||||
|
||||
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
||||
|
||||
@ -58,10 +164,11 @@ abstract class MangaBox(
|
||||
return GET("$baseUrl/$latestUrlPath$page", headers)
|
||||
}
|
||||
|
||||
protected fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
|
||||
private fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select(urlSelector).first()!!.let {
|
||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
url = it.attr("abs:href")
|
||||
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img").first()!!.attr("abs:src")
|
||||
@ -72,62 +179,47 @@ abstract class MangaBox(
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
|
||||
override fun popularMangaNextPageSelector() =
|
||||
"div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
||||
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
|
||||
return if (query.isNotBlank()) {
|
||||
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
|
||||
.addPathSegment(normalizeSearchQuery(query))
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
} else {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
if (getAdvancedGenreFilters().isNotEmpty()) {
|
||||
url.addPathSegment("advanced_search")
|
||||
url.addQueryParameter("page", page.toString())
|
||||
url.addQueryParameter("keyw", normalizeSearchQuery(query))
|
||||
var genreInclude = ""
|
||||
var genreExclude = ""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is KeywordFilter -> filter.toUriPart()?.let { url.addQueryParameter("keyt", it) }
|
||||
is SortFilter -> url.addQueryParameter("orby", filter.toUriPart())
|
||||
is StatusFilter -> url.addQueryParameter("sts", filter.toUriPart())
|
||||
is AdvGenreFilter -> {
|
||||
filter.state.forEach { if (it.isIncluded()) genreInclude += "_${it.id}" }
|
||||
filter.state.forEach { if (it.isExcluded()) genreExclude += "_${it.id}" }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("g_i", genreInclude)
|
||||
url.addQueryParameter("g_e", genreExclude)
|
||||
} else {
|
||||
url.addPathSegment("manga_list")
|
||||
url.addQueryParameter("page", page.toString())
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
||||
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
|
||||
else -> {}
|
||||
}
|
||||
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
|
||||
url.addQueryParameter("page", page.toString())
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
||||
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
GET(url.build(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".panel_story_list .story_item"
|
||||
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
|
||||
override fun searchMangaNextPageSelector() =
|
||||
"a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
|
||||
|
||||
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
|
||||
|
||||
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
|
||||
|
||||
open val descriptionSelector = "div#noidungm, div#panel-story-info-description"
|
||||
open val descriptionSelector = "div#noidungm, div#panel-story-info-description, div#contentBox"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
if (manga.url.startsWith("http")) {
|
||||
@ -146,11 +238,15 @@ abstract class MangaBox(
|
||||
return SManga.create().apply {
|
||||
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
||||
title = infoElement.select("h1, h2").first()!!.text()
|
||||
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a").eachText().joinToString()
|
||||
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
|
||||
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a")
|
||||
.eachText().joinToString()
|
||||
status = parseStatus(
|
||||
infoElement.select("li:contains(status), td:containsOwn(status) + td").text(),
|
||||
)
|
||||
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
|
||||
?.select("a")?.joinToString { it.text() } // kakalot
|
||||
?: infoElement.select("td:containsOwn(genres) + td a").joinToString { it.text() } // nelo
|
||||
?: infoElement.select("td:containsOwn(genres) + td a")
|
||||
.joinToString { it.text() } // nelo
|
||||
} ?: checkForRedirectMessage(document)
|
||||
description = document.select(descriptionSelector).firstOrNull()?.ownText()
|
||||
?.replace("""^$title summary:\s""".toRegex(), "")
|
||||
@ -201,42 +297,21 @@ abstract class MangaBox(
|
||||
|
||||
private fun Element.selectDateFromElement(): Element {
|
||||
val defaultChapterDateSelector = "span"
|
||||
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(alternateChapterDateSelector).last()!!
|
||||
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(
|
||||
alternateChapterDateSelector,
|
||||
).last()!!
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select("a").let {
|
||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
url = it.attr("abs:href")
|
||||
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
name = it.text()
|
||||
scanlator =
|
||||
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
|
||||
}
|
||||
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String, host: String): Long? {
|
||||
return if ("ago" in date) {
|
||||
val value = date.split(' ')[0].toIntOrNull()
|
||||
val cal = Calendar.getInstance()
|
||||
when {
|
||||
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) }
|
||||
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) }
|
||||
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -value) }
|
||||
else -> null
|
||||
}?.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
if (host.contains("manganato", ignoreCase = true)) {
|
||||
// Nelo's date format
|
||||
SimpleDateFormat("MMM dd,yy", Locale.ENGLISH).parse(date)
|
||||
} else {
|
||||
dateformat.parse(date)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}?.time
|
||||
date_upload = dateFormat.tryParse(element.selectDateFromElement().attr("title"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,26 +322,59 @@ abstract class MangaBox(
|
||||
return super.pageListRequest(chapter)
|
||||
}
|
||||
|
||||
open val pageListSelector = "div#vungdoc img, div.container-chapter-reader img"
|
||||
private fun extractArray(scriptContent: String, arrayName: String): List<String> {
|
||||
val pattern = Pattern.compile("$arrayName\\s*=\\s*\\[([^]]+)]")
|
||||
val matcher = pattern.matcher(scriptContent)
|
||||
val arrayValues = mutableListOf<String>()
|
||||
|
||||
if (matcher.find()) {
|
||||
val arrayContent = matcher.group(1)
|
||||
val values = arrayContent?.split(",")
|
||||
if (values != null) {
|
||||
for (value in values) {
|
||||
arrayValues.add(
|
||||
value.trim()
|
||||
.removeSurrounding("\"")
|
||||
.replace("\\/", "/")
|
||||
.removeSuffix("/"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arrayValues
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListSelector)
|
||||
// filter out bad elements for mangakakalots
|
||||
.filterNot { it.attr("src").endsWith("log") }
|
||||
.mapIndexed { i, element ->
|
||||
val url = element.attr("abs:src").let { src ->
|
||||
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
|
||||
"https://images.weserv.nl/?url=" + src.substringAfter("//")
|
||||
} else {
|
||||
src
|
||||
}
|
||||
}
|
||||
Page(i, document.location(), url)
|
||||
val element = document.select("head > script").lastOrNull()
|
||||
?: return emptyList()
|
||||
val cdns =
|
||||
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
|
||||
val chapterImages = extractArray(element.html(), "chapterImages")
|
||||
|
||||
// Add all parsed cdns to set
|
||||
cdnSet.addAll(cdns)
|
||||
|
||||
return chapterImages.mapIndexed { i, imagePath ->
|
||||
val parsedUrl = cdns[0].toHttpUrl().run {
|
||||
newBuilder()
|
||||
.encodedPath(
|
||||
"/$imagePath".replace(
|
||||
"//",
|
||||
"/",
|
||||
),
|
||||
) // replace ensures that there's at least one trailing slash prefix
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
Page(i, document.location(), parsedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
|
||||
return GET(page.imageUrl!!, headers).newBuilder()
|
||||
.tag(MangaBoxFallBackTag::class.java, MangaBoxFallBackTag()).build()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
@ -282,46 +390,27 @@ abstract class MangaBox(
|
||||
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
||||
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
||||
str = str.replace("đ".toRegex(), "d")
|
||||
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
|
||||
str = str.replace(
|
||||
"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(),
|
||||
"_",
|
||||
)
|
||||
str = str.replace("_+_".toRegex(), "_")
|
||||
str = str.replace("""^_+|_+$""".toRegex(), "")
|
||||
return str
|
||||
}
|
||||
|
||||
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
|
||||
FilterList(
|
||||
KeywordFilter(getKeywordFilters()),
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
AdvGenreFilter(getAdvancedGenreFilters()),
|
||||
)
|
||||
} else {
|
||||
FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
GenreFilter(getGenreFilters()),
|
||||
)
|
||||
}
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
GenreFilter(getGenreFilters()),
|
||||
)
|
||||
|
||||
private class KeywordFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Keyword search ", vals)
|
||||
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
|
||||
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
|
||||
|
||||
// For advanced search, specifically tri-state genres
|
||||
private class AdvGenreFilter(vals: List<AdvGenre>) : Filter.Group<AdvGenre>("Category", vals)
|
||||
class AdvGenre(val id: String?, name: String) : Filter.TriState(name)
|
||||
|
||||
// keyt query parameter
|
||||
private fun getKeywordFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair(null, "Everything"),
|
||||
Pair("title", "Title"),
|
||||
Pair("alternative", "Alt title"),
|
||||
Pair("author", "Author"),
|
||||
)
|
||||
|
||||
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("latest", "Latest"),
|
||||
Pair("newest", "Newest"),
|
||||
@ -337,53 +426,72 @@ abstract class MangaBox(
|
||||
|
||||
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("all", "ALL"),
|
||||
Pair("2", "Action"),
|
||||
Pair("3", "Adult"),
|
||||
Pair("4", "Adventure"),
|
||||
Pair("6", "Comedy"),
|
||||
Pair("7", "Cooking"),
|
||||
Pair("9", "Doujinshi"),
|
||||
Pair("10", "Drama"),
|
||||
Pair("11", "Ecchi"),
|
||||
Pair("12", "Fantasy"),
|
||||
Pair("13", "Gender bender"),
|
||||
Pair("14", "Harem"),
|
||||
Pair("15", "Historical"),
|
||||
Pair("16", "Horror"),
|
||||
Pair("45", "Isekai"),
|
||||
Pair("17", "Josei"),
|
||||
Pair("44", "Manhua"),
|
||||
Pair("43", "Manhwa"),
|
||||
Pair("19", "Martial arts"),
|
||||
Pair("20", "Mature"),
|
||||
Pair("21", "Mecha"),
|
||||
Pair("22", "Medical"),
|
||||
Pair("24", "Mystery"),
|
||||
Pair("25", "One shot"),
|
||||
Pair("26", "Psychological"),
|
||||
Pair("27", "Romance"),
|
||||
Pair("28", "School life"),
|
||||
Pair("29", "Sci fi"),
|
||||
Pair("30", "Seinen"),
|
||||
Pair("31", "Shoujo"),
|
||||
Pair("32", "Shoujo ai"),
|
||||
Pair("33", "Shounen"),
|
||||
Pair("34", "Shounen ai"),
|
||||
Pair("35", "Slice of life"),
|
||||
Pair("36", "Smut"),
|
||||
Pair("37", "Sports"),
|
||||
Pair("38", "Supernatural"),
|
||||
Pair("39", "Tragedy"),
|
||||
Pair("40", "Webtoons"),
|
||||
Pair("41", "Yaoi"),
|
||||
Pair("42", "Yuri"),
|
||||
Pair("action", "Action"),
|
||||
Pair("adult", "Adult"),
|
||||
Pair("adventure", "Adventure"),
|
||||
Pair("comedy", "Comedy"),
|
||||
Pair("cooking", "Cooking"),
|
||||
Pair("doujinshi", "Doujinshi"),
|
||||
Pair("drama", "Drama"),
|
||||
Pair("ecchi", "Ecchi"),
|
||||
Pair("fantasy", "Fantasy"),
|
||||
Pair("gender-bender", "Gender bender"),
|
||||
Pair("harem", "Harem"),
|
||||
Pair("historical", "Historical"),
|
||||
Pair("horror", "Horror"),
|
||||
Pair("isekai", "Isekai"),
|
||||
Pair("josei", "Josei"),
|
||||
Pair("manhua", "Manhua"),
|
||||
Pair("manhwa", "Manhwa"),
|
||||
Pair("martial-arts", "Martial arts"),
|
||||
Pair("mature", "Mature"),
|
||||
Pair("mecha", "Mecha"),
|
||||
Pair("medical", "Medical"),
|
||||
Pair("mystery", "Mystery"),
|
||||
Pair("one-shot", "One shot"),
|
||||
Pair("psychological", "Psychological"),
|
||||
Pair("romance", "Romance"),
|
||||
Pair("school-life", "School life"),
|
||||
Pair("sci-fi", "Sci fi"),
|
||||
Pair("seinen", "Seinen"),
|
||||
Pair("shoujo", "Shoujo"),
|
||||
Pair("shoujo-ai", "Shoujo ai"),
|
||||
Pair("shounen", "Shounen"),
|
||||
Pair("shounen-ai", "Shounen ai"),
|
||||
Pair("slice-of-life", "Slice of life"),
|
||||
Pair("smut", "Smut"),
|
||||
Pair("sports", "Sports"),
|
||||
Pair("supernatural", "Supernatural"),
|
||||
Pair("tragedy", "Tragedy"),
|
||||
Pair("webtoons", "Webtoons"),
|
||||
Pair("yaoi", "Yaoi"),
|
||||
Pair("yuri", "Yuri"),
|
||||
)
|
||||
|
||||
// To be overridden if using tri-state genres
|
||||
protected open fun getAdvancedGenreFilters(): List<AdvGenre> = emptyList()
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_USE_MIRROR
|
||||
title = "Mirror"
|
||||
entries = mirrorEntries
|
||||
entryValues = mirrorEntries.map { "${URL_PREFIX}$it" }.toTypedArray()
|
||||
setDefaultValue(entryValues[0])
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
// Update values
|
||||
mirror = newValue as String
|
||||
true
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_USE_MIRROR = "pref_use_mirror"
|
||||
private const val URL_PREFIX = "https://"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||
|
||||
class MangaBoxLinkedCdnSet : LinkedHashSet<String>() {
|
||||
fun moveItemToFirst(item: String) {
|
||||
// Lock the object to avoid multi threading issues
|
||||
synchronized(this) {
|
||||
if (this.contains(item) && this.first() != item) {
|
||||
// Remove the item from the current set
|
||||
this.remove(item)
|
||||
// Create a new list with the item at the first position
|
||||
val newItems = mutableListOf(item)
|
||||
// Add the remaining items
|
||||
newItems.addAll(this)
|
||||
// Clear the current set and add all items from the new list
|
||||
this.clear()
|
||||
this.addAll(newItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -48,7 +48,7 @@ abstract class MangaEsp(
|
||||
|
||||
protected open val useApiSearch = false
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 29
|
||||
baseVersionCode = 30
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
|
@ -17,7 +17,6 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -31,7 +30,7 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
@ -49,6 +48,7 @@ abstract class MangaHub(
|
||||
|
||||
private var baseApiUrl = "https://api.mghcdn.com"
|
||||
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
||||
private val regex = Regex("mhub_access=([^;]+)")
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
@ -91,8 +91,6 @@ abstract class MangaHub(
|
||||
}
|
||||
|
||||
private fun refreshApiKey(chapter: SChapter) {
|
||||
val now = Calendar.getInstance().time.time
|
||||
|
||||
val slug = "$baseUrl${chapter.url}"
|
||||
.toHttpUrlOrNull()
|
||||
?.pathSegments
|
||||
@ -104,32 +102,28 @@ abstract class MangaHub(
|
||||
baseUrl.toHttpUrl()
|
||||
}
|
||||
|
||||
// Clear key cookie
|
||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
||||
val oldKey = client.cookieJar
|
||||
.loadForRequest(baseUrl.toHttpUrl())
|
||||
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
||||
|
||||
// Set required cookie (for cache busting?)
|
||||
val recently = buildJsonObject {
|
||||
putJsonObject((now - (0..3600).random()).toString()) {
|
||||
put("mangaID", (1..42_000).random())
|
||||
put("number", (1..20).random())
|
||||
for (i in 1..2) {
|
||||
// Clear key cookie
|
||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
||||
|
||||
// We try requesting again with param if the first one fails
|
||||
val query = if (i == 2) "?reloadKey=1" else ""
|
||||
|
||||
try {
|
||||
val response = client.newCall(GET("$url$query", headers)).execute()
|
||||
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
|
||||
response.close() // Avoid potential resource leaks
|
||||
|
||||
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
|
||||
} catch (_: IOException) {
|
||||
throw IOException("An error occurred while obtaining a new API key") // Show error
|
||||
}
|
||||
}.toString()
|
||||
|
||||
client.cookieJar.saveFromResponse(
|
||||
url,
|
||||
listOf(
|
||||
Cookie.Builder()
|
||||
.domain(url.host)
|
||||
.name("recently")
|
||||
.value(URLEncoder.encode(recently, "utf-8"))
|
||||
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
|
||||
val request = GET("$url?reloadKey=1", headers)
|
||||
client.newCall(request).execute()
|
||||
}
|
||||
}
|
||||
|
||||
data class SMangaDTO(
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@ -16,8 +16,6 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.ref.SoftReference
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@ -34,14 +32,12 @@ abstract class MangaThemesiaAlt(
|
||||
protected open val listUrl = "$mangaUrlDirectory/list-mode/"
|
||||
protected open val listSelector = "div#content div.soralist ul li a.series"
|
||||
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).also {
|
||||
if (it.contains("__random_part_cache")) {
|
||||
it.edit().remove("__random_part_cache").apply()
|
||||
}
|
||||
if (it.contains("titles_without_random_part")) {
|
||||
it.edit().remove("titles_without_random_part").apply()
|
||||
}
|
||||
protected val preferences by getPreferencesLazy {
|
||||
if (contains("__random_part_cache")) {
|
||||
edit().remove("__random_part_cache").apply()
|
||||
}
|
||||
if (contains("titles_without_random_part")) {
|
||||
edit().remove("titles_without_random_part").apply()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 7
|
||||
|
@ -33,7 +33,7 @@ open class MCCMS(
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client by lazy {
|
||||
network.client.newBuilder()
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ open class MCCMSWeb(
|
||||
override val supportsLatest get() = true
|
||||
|
||||
override val client by lazy {
|
||||
network.client.newBuilder()
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
}
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
@ -26,7 +26,7 @@ abstract class MultiChan(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.rateLimit(2)
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
@ -40,7 +40,7 @@ abstract class Senkuro(
|
||||
.add("Content-Type", "application/json")
|
||||
|
||||
override val client: OkHttpClient =
|
||||
network.client.newBuilder()
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.build()
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 11
|
||||
baseVersionCode = 12
|
||||
|
@ -34,7 +34,7 @@ abstract class SinMH(
|
||||
protected open val mobileUrl = _baseUrl.replaceFirst("www.", "m.")
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.client.newBuilder().rateLimit(2).build()
|
||||
override val client = network.cloudflareClient.newBuilder().rateLimit(2).build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", System.getProperty("http.agent")!!)
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 4
|
||||
|
@ -45,7 +45,7 @@ open class Webtoons(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.cookieJar(
|
||||
object : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
||||
@ -197,7 +197,7 @@ open class Webtoons(
|
||||
open fun parseDetailsThumbnail(document: Document): String? {
|
||||
val picElement = document.select("#content > div.cont_box > div.detail_body")
|
||||
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
|
||||
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")")
|
||||
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")").removeSurrounding("\"").removeSurrounding("'")
|
||||
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".pt.blackscans.BlackScansUrlActivity"
|
||||
android:name="eu.kanade.tachiyomi.multisrc.yuyu.YuYuUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
@ -11,11 +11,10 @@
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="blackscans.site"
|
||||
android:pathPattern="/series/..*"
|
||||
android:scheme="https" />
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
5
lib-multisrc/yuyu/build.gradle.kts
Normal file
5
lib-multisrc/yuyu/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
206
lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt
Normal file
206
lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt
Normal file
@ -0,0 +1,206 @@
|
||||
package eu.kanade.tachiyomi.multisrc.yuyu
|
||||
|
||||
import android.net.Uri
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.net.URLEncoder
|
||||
|
||||
abstract class YuYu(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector() = ".top10-section .top10-item a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
title = element.selectFirst("h3")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
// ============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("pagina", page.toString())
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = ".manga-list .manga-card"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.page-link:contains(>)"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
||||
val url = element.selectFirst("a.manga-cover")!!.absUrl("href")
|
||||
val uri = Uri.parse(url)
|
||||
val pathSegments = uri.pathSegments
|
||||
val lastSegment = URLEncoder.encode(pathSegments.last(), "UTF-8")
|
||||
val encodedUrl = uri.buildUpon()
|
||||
.path(pathSegments.dropLast(1).joinToString("/") + "/$lastSegment")
|
||||
.toString()
|
||||
|
||||
title = element.selectFirst("a.manga-title")!!.text()
|
||||
thumbnail_url = element.selectFirst("a.manga-cover img")?.absUrl("data-src")
|
||||
setUrlWithoutDomain(encodedUrl)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
|
||||
return MangasPage(mangas, document.hasNextPage())
|
||||
}
|
||||
|
||||
private fun Document.hasNextPage() =
|
||||
selectFirst(latestUpdatesNextPageSelector())?.absUrl("href")?.let {
|
||||
selectFirst("a.page-link.active")
|
||||
?.absUrl("href")
|
||||
.equals(it, ignoreCase = true).not()
|
||||
} ?: false
|
||||
|
||||
// ============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("search", query)
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val slug = query.substringAfter(PREFIX_SEARCH)
|
||||
return client.newCall(GET("$baseUrl/manga/$slug", headers))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val manga = mangaDetailsParse(it.asJsoup())
|
||||
MangasPage(listOf(manga), false)
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".search-result-item"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
title = element.selectFirst(".search-result-title")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
setUrlWithoutDomain(
|
||||
element.attr("onclick").let {
|
||||
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
// ============================== Manga Details =========================
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val details = document.selectFirst(".manga-banner .container")!!
|
||||
title = details.selectFirst("h1")!!.text()
|
||||
thumbnail_url = details.selectFirst("img")?.absUrl("src")
|
||||
genre = details.select(".genre-tag").joinToString { it.text() }
|
||||
description = details.selectFirst(".sinopse p")?.text()
|
||||
details.selectFirst(".manga-meta > div")?.ownText()?.let {
|
||||
status = when (it.lowercase()) {
|
||||
"em andamento" -> SManga.ONGOING
|
||||
"completo" -> SManga.COMPLETED
|
||||
"cancelado" -> SManga.CANCELLED
|
||||
"hiato" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
setUrlWithoutDomain(document.location())
|
||||
}
|
||||
|
||||
private fun SManga.fetchMangaId(): String {
|
||||
val document = client.newCall(mangaDetailsRequest(this)).execute().asJsoup()
|
||||
return document.select("script")
|
||||
.map(Element::data)
|
||||
.firstOrNull(MANGA_ID_REGEX::containsMatchIn)
|
||||
?.let { MANGA_ID_REGEX.find(it)?.groups?.get(1)?.value }
|
||||
?: throw Exception("Manga ID não encontrado")
|
||||
}
|
||||
|
||||
// ============================== Chapters ===============================
|
||||
|
||||
override fun chapterListSelector() = "a.chapter-item"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
name = element.selectFirst(".capitulo-numero")!!.ownText()
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val mangaId = manga.fetchMangaId()
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
var page = 1
|
||||
do {
|
||||
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
|
||||
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
|
||||
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
|
||||
} while (dto.hasNext())
|
||||
return Observable.just(chapters)
|
||||
}
|
||||
|
||||
private fun fetchChapterListPage(mangaId: String, page: Int): Response {
|
||||
val url = "$baseUrl/ajax/lzmvke.php?order=DESC".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("manga_id", mangaId)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return client
|
||||
.newCall(GET(url, headers))
|
||||
.execute()
|
||||
}
|
||||
|
||||
// ============================== Pages ===============================
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("picture img").mapIndexed { idx, element ->
|
||||
Page(idx, imageUrl = element.absUrl("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
// ============================== Utilities ===========================
|
||||
|
||||
@Serializable
|
||||
class ChaptersDto(val chapters: String, private val remaining: Int) {
|
||||
fun hasNext() = remaining > 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
val SEARCH_URL_REGEX = "'([^']+)".toRegex()
|
||||
val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.yushukemangas
|
||||
package eu.kanade.tachiyomi.multisrc.yuyu
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
@ -7,7 +7,7 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class YushukeMangasUrlActivity : Activity() {
|
||||
class YuYuUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
@ -17,7 +17,7 @@ class YushukeMangasUrlActivity : Activity() {
|
||||
if (pathSegment != null && pathSegment.size > 1) {
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
|
||||
putExtra("query", "${YuYu.PREFIX_SEARCH}${pathSegment[1]}")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Akuma'
|
||||
extClass = '.AkumaFactory'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 7
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.akuma
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
@ -25,8 +25,6 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@ -48,12 +46,12 @@ class Akuma(
|
||||
|
||||
private var storedToken: String? = null
|
||||
|
||||
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
|
||||
private val ddosGuardIntercept = DDosGuardInterceptor(network.cloudflareClient)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(ddosGuardIntercept)
|
||||
.addInterceptor(::tokenInterceptor)
|
||||
.rateLimit(2)
|
||||
@ -112,9 +110,7 @@ class Akuma(
|
||||
return storedToken!!
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
private val displayFullTitle: Boolean get() = preferences.getBoolean(PREF_TITLE, false)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 48
|
||||
extVersionCode = 49
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@ -48,10 +49,7 @@ open class BatoTo(
|
||||
private val siteLang: String,
|
||||
) : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.migrateMirrorPref()
|
||||
}
|
||||
private val preferences by getPreferencesLazy { migrateMirrorPref() }
|
||||
|
||||
override val name: String = "Bato.to"
|
||||
override val baseUrl: String get() = mirror
|
||||
@ -125,14 +123,12 @@ open class BatoTo(
|
||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
|
||||
private fun SharedPreferences.migrateMirrorPref() {
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Comic Fury'
|
||||
extClass = '.ComicFuryFactory'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicfury
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -16,13 +15,12 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@ -35,7 +33,7 @@ class ComicFury(
|
||||
override val name: String = "Comic Fury$extraName" // Used for No Text
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build()
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(TextInterceptor()).build()
|
||||
|
||||
/**
|
||||
* Archive is on a separate page from manga info
|
||||
@ -205,9 +203,7 @@ class ComicFury(
|
||||
private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
|
||||
|
||||
// START OF AUTHOR NOTES //
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
companion object {
|
||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ ext {
|
||||
themePkg = 'gigaviewer'
|
||||
baseUrl = 'https://comic-growl.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,5 +1,8 @@
|
||||
ignored_groups_title=Ignored Groups
|
||||
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
||||
show_alternative_titles_title=Show Alternative Titles
|
||||
show_alternative_titles_on=Adds alternative titles to the description
|
||||
show_alternative_titles_off=Does not show alternative titles to the description
|
||||
include_tags_title=Include Tags
|
||||
include_tags_on=More specific, but might contain spoilers!
|
||||
include_tags_off=Only the broader genres
|
||||
@ -9,6 +12,9 @@ group_tags_off=List all tags together
|
||||
update_cover_title=Update Covers
|
||||
update_cover_on=Keep cover updated
|
||||
update_cover_off=Prefer first cover
|
||||
local_title_title=Translated Title
|
||||
local_title_on=if available
|
||||
local_title_off=Use the default title from the site
|
||||
score_position_title=Score Position in the Description
|
||||
score_position_top=Top
|
||||
score_position_middle=Middle
|
||||
|
@ -1,5 +1,8 @@
|
||||
ignored_groups_title=Grupos Ignorados
|
||||
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
|
||||
show_alternative_titles_title=Mostrar Títulos Alternativos
|
||||
show_alternative_titles_on=Adiciona títulos alternativos à descrição
|
||||
show_alternative_titles_off=Não mostra títulos alternativos na descrição
|
||||
include_tags_title=Incluir Tags
|
||||
include_tags_on=Mais detalhadas, mas podem conter spoilers
|
||||
include_tags_off=Apenas os gêneros básicos
|
||||
@ -9,6 +12,9 @@ group_tags_off=Listar todas as tags juntas
|
||||
update_cover_title=Atualizar Capas
|
||||
update_cover_on=Manter capas atualizadas
|
||||
update_cover_off=Usar apenas a primeira capa
|
||||
local_title_title=Título Traduzido
|
||||
local_title_on=se disponível
|
||||
local_title_off=Usar o título padrão do site
|
||||
score_position_title=Posição da Nota na Descrição
|
||||
score_position_top=Topo
|
||||
score_position_middle=Meio
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 52
|
||||
extVersionCode = 55
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
@ -17,16 +16,16 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Builder
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@ -65,10 +64,7 @@ abstract class Comick(
|
||||
)
|
||||
}
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.newLineIgnoredGroups()
|
||||
}
|
||||
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
@ -83,6 +79,20 @@ abstract class Comick(
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = SHOW_ALTERNATIVE_TITLES_PREF
|
||||
title = intl["show_alternative_titles_title"]
|
||||
summaryOn = intl["show_alternative_titles_on"]
|
||||
summaryOff = intl["show_alternative_titles_off"]
|
||||
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit()
|
||||
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
|
||||
.commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = INCLUDE_MU_TAGS_PREF
|
||||
title = intl["include_tags_title"]
|
||||
@ -125,6 +135,20 @@ abstract class Comick(
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = LOCAL_TITLE_PREF
|
||||
title = intl["local_title_title"]
|
||||
summaryOff = intl["local_title_off"]
|
||||
summaryOn = intl["local_title_on"]
|
||||
setDefaultValue(LOCAL_TITLE_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit()
|
||||
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
|
||||
.commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = SCORE_POSITION_PREF
|
||||
title = intl["score_position_title"]
|
||||
@ -160,6 +184,9 @@ abstract class Comick(
|
||||
.orEmpty()
|
||||
.toSet()
|
||||
|
||||
private val SharedPreferences.showAlternativeTitles: Boolean
|
||||
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
||||
|
||||
private val SharedPreferences.includeMuTags: Boolean
|
||||
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
|
||||
|
||||
@ -169,6 +196,17 @@ abstract class Comick(
|
||||
private val SharedPreferences.updateCover: Boolean
|
||||
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
|
||||
|
||||
private val SharedPreferences.localTitle: String
|
||||
get() = if (getBoolean(
|
||||
LOCAL_TITLE_PREF,
|
||||
LOCAL_TITLE_DEFAULT,
|
||||
)
|
||||
) {
|
||||
comickLang.lowercase()
|
||||
} else {
|
||||
"all"
|
||||
}
|
||||
|
||||
private val SharedPreferences.scorePosition: String
|
||||
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
|
||||
|
||||
@ -277,6 +315,16 @@ abstract class Comick(
|
||||
return MangasPage(entries, end < searchResponse.size)
|
||||
}
|
||||
|
||||
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
|
||||
tags.split(",").forEach {
|
||||
builder.addQueryParameter(
|
||||
parameterName,
|
||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
|
||||
filters.forEach { it ->
|
||||
@ -298,7 +346,7 @@ abstract class Comick(
|
||||
}
|
||||
|
||||
is DemographicFilter -> {
|
||||
it.state.filter { it.isIncluded() }.forEach {
|
||||
it.state.filter { it.state }.forEach {
|
||||
addQueryParameter("demographic", it.value)
|
||||
}
|
||||
}
|
||||
@ -319,6 +367,12 @@ abstract class Comick(
|
||||
}
|
||||
}
|
||||
|
||||
is ContentRatingFilter -> {
|
||||
if (it.state > 0) {
|
||||
addQueryParameter("content_rating", it.getValue())
|
||||
}
|
||||
}
|
||||
|
||||
is CreatedAtFilter -> {
|
||||
if (it.state > 0) {
|
||||
addQueryParameter("time", it.getValue())
|
||||
@ -345,13 +399,13 @@ abstract class Comick(
|
||||
|
||||
is TagFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
it.state.split(",").forEach {
|
||||
addQueryParameter(
|
||||
"tags",
|
||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
||||
)
|
||||
}
|
||||
addTagQueryParameters(this, it.state, "tags")
|
||||
}
|
||||
}
|
||||
|
||||
is ExcludedTagFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
addTagQueryParameters(this, it.state, "excluded-tags")
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,14 +459,18 @@ abstract class Comick(
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
showAlternativeTitles = preferences.showAlternativeTitles,
|
||||
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
||||
groupTags = preferences.groupTags,
|
||||
titleLang = preferences.localTitle,
|
||||
)
|
||||
}
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
showAlternativeTitles = preferences.showAlternativeTitles,
|
||||
groupTags = preferences.groupTags,
|
||||
titleLang = preferences.localTitle,
|
||||
)
|
||||
}
|
||||
|
||||
@ -507,8 +565,9 @@ abstract class Comick(
|
||||
|
||||
override fun getFilterList() = getFilters()
|
||||
|
||||
private fun SharedPreferences.newLineIgnoredGroups(): SharedPreferences {
|
||||
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return this
|
||||
private fun SharedPreferences.newLineIgnoredGroups() {
|
||||
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
|
||||
|
||||
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
|
||||
|
||||
edit()
|
||||
@ -522,14 +581,14 @@ abstract class Comick(
|
||||
)
|
||||
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
|
||||
.apply()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SLUG_SEARCH_PREFIX = "id:"
|
||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
|
||||
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
|
||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||
private const val GROUP_TAGS_PREF = "GroupTags"
|
||||
@ -539,6 +598,8 @@ abstract class Comick(
|
||||
private const val FIRST_COVER_DEFAULT = true
|
||||
private const val SCORE_POSITION_PREF = "ScorePosition"
|
||||
const val SCORE_POSITION_DEFAULT = "top"
|
||||
private const val LOCAL_TITLE_PREF = "LocalTitle"
|
||||
private const val LOCAL_TITLE_DEFAULT = false
|
||||
private const val LIMIT = 20
|
||||
private const val CHAPTERS_LIMIT = 99999
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.all.comickfun
|
||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
|
||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
|
||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
|
||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
@ -36,13 +37,20 @@ class Manga(
|
||||
fun toSManga(
|
||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
||||
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
|
||||
covers: List<MDcovers>? = null,
|
||||
groupTags: Boolean = GROUP_TAGS_DEFAULT,
|
||||
) =
|
||||
SManga.create().apply {
|
||||
titleLang: String,
|
||||
): SManga {
|
||||
val entryTitle = comic.altTitles.firstOrNull {
|
||||
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
|
||||
}?.title ?: comic.title
|
||||
val titles = listOf(Title(title = comic.title)) + comic.altTitles
|
||||
|
||||
return SManga.create().apply {
|
||||
// appennding # at end as part of migration from slug to hid
|
||||
url = "/comic/${comic.hid}#"
|
||||
title = comic.title
|
||||
title = entryTitle
|
||||
description = buildString {
|
||||
if (scorePosition == "top") append(comic.fancyScore)
|
||||
val desc = comic.desc?.beautifyDescription()
|
||||
@ -54,13 +62,14 @@ class Manga(
|
||||
if (this.isNotEmpty()) append("\n\n")
|
||||
append(comic.fancyScore)
|
||||
}
|
||||
if (comic.altTitles.isNotEmpty()) {
|
||||
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
|
||||
if (this.isNotEmpty()) append("\n\n")
|
||||
append("Alternative Titles:\n")
|
||||
append(
|
||||
comic.altTitles.mapNotNull { title ->
|
||||
title.title?.let { "• $it" }
|
||||
}.joinToString("\n"),
|
||||
titles.distinctBy { it.title }.filter { it.title != entryTitle }
|
||||
.mapNotNull { title ->
|
||||
title.title?.let { "• $it" }
|
||||
}.joinToString("\n"),
|
||||
)
|
||||
}
|
||||
if (scorePosition == "bottom") {
|
||||
@ -95,6 +104,7 @@ class Manga(
|
||||
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
||||
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@ -169,6 +179,7 @@ class MDcovers(
|
||||
@Serializable
|
||||
class Title(
|
||||
val title: String?,
|
||||
val lang: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -11,6 +11,7 @@ fun getFilters(): FilterList {
|
||||
TypeFilter("Type", getTypeList),
|
||||
SortFilter("Sort", getSortsList),
|
||||
StatusFilter("Status", getStatusList),
|
||||
ContentRatingFilter("Content Rating", getContentRatingList),
|
||||
CompletedFilter("Completely Scanlated?"),
|
||||
CreatedAtFilter("Created at", getCreatedAtList),
|
||||
MinimumFilter("Minimum Chapters"),
|
||||
@ -20,6 +21,7 @@ fun getFilters(): FilterList {
|
||||
ToYearFilter("To"),
|
||||
Filter.Header("Separate tags with commas"),
|
||||
TagFilter("Tags"),
|
||||
ExcludedTagFilter("Excluded Tags"),
|
||||
)
|
||||
}
|
||||
|
||||
@ -29,8 +31,10 @@ internal class GenreFilter(name: String, genreList: List<Pair<String, String>>)
|
||||
|
||||
internal class TagFilter(name: String) : TextFilter(name)
|
||||
|
||||
internal class ExcludedTagFilter(name: String) : TextFilter(name)
|
||||
|
||||
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
|
||||
Filter.Group<TriFilter>(name, demographicList.map { TriFilter(it.first, it.second) })
|
||||
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
|
||||
|
||||
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
|
||||
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
|
||||
@ -52,6 +56,9 @@ internal class SortFilter(name: String, sortList: List<Pair<String, String>>, st
|
||||
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
||||
SelectFilter(name, statusList, state)
|
||||
|
||||
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
||||
SelectFilter(name, statusList, state)
|
||||
|
||||
/** Generics **/
|
||||
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
@ -156,12 +163,14 @@ private val getDemographicList: List<Pair<String, String>> = listOf(
|
||||
Pair("Shoujo", "2"),
|
||||
Pair("Seinen", "3"),
|
||||
Pair("Josei", "4"),
|
||||
Pair("None", "5"),
|
||||
)
|
||||
|
||||
private val getTypeList: List<Pair<String, String>> = listOf(
|
||||
Pair("Manga", "jp"),
|
||||
Pair("Manhwa", "kr"),
|
||||
Pair("Manhua", "cn"),
|
||||
Pair("Others", "others"),
|
||||
)
|
||||
|
||||
private val getCreatedAtList: List<Pair<String, String>> = listOf(
|
||||
@ -190,3 +199,10 @@ private val getStatusList: List<Pair<String, String>> = listOf(
|
||||
Pair("Cancelled", "3"),
|
||||
Pair("Hiatus", "4"),
|
||||
)
|
||||
|
||||
private val getContentRatingList: List<Pair<String, String>> = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("Safe", "safe"),
|
||||
Pair("Suggestive", "suggestive"),
|
||||
Pair("Erotica", "erotica"),
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Comico'
|
||||
extClass = '.ComicoFactory'
|
||||
extVersionCode = 6
|
||||
extVersionCode = 7
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ open class Comico(
|
||||
this["Origin"] = baseUrl
|
||||
}.build()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.cookieJar(
|
||||
object : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'ComicsKingdom'
|
||||
extClass = '.ComicsKingdomFactory'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicskingdom
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -12,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl
|
||||
@ -19,8 +19,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@ -301,9 +299,7 @@ class ComicsKingdom(override val lang: String) : ConfigurableSource, HttpSource(
|
||||
screen.addPreference(compactpref)
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
private fun shouldCompact() = preferences.getBoolean("compactPref", true)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = "Comikey"
|
||||
extClass = ".ComikeyFactory"
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl
|
||||
@ -75,9 +76,7 @@ open class Comikey(
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences by getPreferencesLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Danbooru'
|
||||
extClass = '.Danbooru'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,51 +1,46 @@
|
||||
package eu.kanade.tachiyomi.extension.all.danbooru
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Danbooru : ParsedHttpSource() {
|
||||
class Danbooru : HttpSource(), ConfigurableSource {
|
||||
override val name: String = "Danbooru"
|
||||
override val baseUrl: String = "https://danbooru.donmai.us"
|
||||
override val lang: String = "all"
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
private val json: Json by injectLazy()
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val dateFormat: SimpleDateFormat by lazy {
|
||||
private val dateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private val preference by getPreferencesLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList())
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga =
|
||||
searchMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector(): String =
|
||||
searchMangaNextPageSelector()
|
||||
|
||||
override fun popularMangaSelector(): String =
|
||||
searchMangaSelector()
|
||||
override fun popularMangaParse(response: Response) =
|
||||
searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
|
||||
@ -87,86 +82,100 @@ class Danbooru : ParsedHttpSource() {
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
"article.post-preview"
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
url = element.selectFirst(".post-preview-link")?.attr("href")!!
|
||||
title = element.selectFirst("div.text-center")?.text() ?: ""
|
||||
val entries = document.select("article.post-preview").map {
|
||||
searchMangaFromElement(it)
|
||||
}
|
||||
val hasNextPage = document.selectFirst("a.paginator-next") != null
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
url = element.selectFirst(".post-preview-link")!!.attr("href")
|
||||
title = element.selectFirst("div.text-center")!!.text()
|
||||
|
||||
thumbnail_url = element.selectFirst("source")?.attr("srcset")
|
||||
?.substringAfterLast(',')?.trim()
|
||||
?.substringBeforeLast(' ')?.trimStart()
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
"a.paginator-next"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList(FilterOrder("created_at")))
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
searchMangaSelector()
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
searchMangaFromElement(element)
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
searchMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
|
||||
title = document.selectFirst(".pool-category-series, .pool-category-collection")?.text() ?: ""
|
||||
description = document.getElementById("description")?.wholeText() ?: ""
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
title = document.selectFirst(".pool-category-series, .pool-category-collection")!!.text()
|
||||
description = document.getElementById("description")?.wholeText()
|
||||
author = document.selectFirst("#description a[href*=artists]")?.ownText()
|
||||
artist = author
|
||||
update_strategy = if (!preference.splitChaptersPref) {
|
||||
UpdateStrategy.ONLY_FETCH_ONCE
|
||||
} else {
|
||||
UpdateStrategy.ALWAYS_UPDATE
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
GET("$baseUrl${manga.url}.json?only=id,created_at", headers)
|
||||
GET("$baseUrl${manga.url}.json", headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> = listOf(
|
||||
SChapter.create().apply {
|
||||
val data = json.decodeFromString<JsonObject>(response.body.string())
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<Pool>()
|
||||
|
||||
val id = data["id"]!!.jsonPrimitive.content
|
||||
val createdAt = data["created_at"]?.jsonPrimitive?.content
|
||||
|
||||
url = "/pools/$id"
|
||||
name = "Oneshot"
|
||||
date_upload = createdAt?.let(::parseTimestamp) ?: 0
|
||||
chapter_number = 0F
|
||||
},
|
||||
)
|
||||
|
||||
override fun chapterListSelector(): String =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter =
|
||||
throw IllegalStateException("Not used")
|
||||
return if (preference.splitChaptersPref) {
|
||||
data.postIds.mapIndexed { index, id ->
|
||||
SChapter.create().apply {
|
||||
url = "/posts/$id"
|
||||
name = "Post ${index + 1}"
|
||||
chapter_number = index + 1f
|
||||
}
|
||||
}.reversed().apply {
|
||||
if (isNotEmpty()) {
|
||||
this[0].date_upload = dateFormat.tryParse(data.updatedAt)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = "/pools/${data.id}"
|
||||
name = "Oneshot"
|
||||
date_upload = dateFormat.tryParse(data.updatedAt)
|
||||
chapter_number = 0F
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request =
|
||||
GET("$baseUrl${chapter.url}.json?only=post_ids", headers)
|
||||
GET("$baseUrl${chapter.url}.json", headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> =
|
||||
json.decodeFromString<JsonObject>(response.body.string())
|
||||
.get("post_ids")?.jsonArray
|
||||
?.map { it.jsonPrimitive.content }
|
||||
?.mapIndexed { i, id -> Page(index = i, url = "/posts/$id") }
|
||||
?: emptyList()
|
||||
if (response.request.url.toString().contains("/posts/")) {
|
||||
val data = response.parseAs<Post>()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> =
|
||||
throw IllegalStateException("Not used")
|
||||
listOf(
|
||||
Page(index = 0, imageUrl = data.fileUrl),
|
||||
)
|
||||
} else {
|
||||
val data = response.parseAs<Pool>()
|
||||
|
||||
data.postIds.mapIndexed { index, id ->
|
||||
Page(index, url = "/posts/$id")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request =
|
||||
GET("$baseUrl${page.url}.json?only=file_url", headers)
|
||||
GET("$baseUrl${page.url}.json", headers)
|
||||
|
||||
override fun imageUrlParse(response: Response): String =
|
||||
json.decodeFromString<JsonObject>(response.body.string())
|
||||
.get("file_url")!!.jsonPrimitive.content
|
||||
|
||||
override fun imageUrlParse(document: Document): String =
|
||||
throw IllegalStateException("Not used")
|
||||
response.parseAs<Post>().fileUrl
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String =
|
||||
baseUrl + chapter.url
|
||||
@ -181,6 +190,20 @@ class Danbooru : ParsedHttpSource() {
|
||||
),
|
||||
)
|
||||
|
||||
private fun parseTimestamp(string: String): Long? =
|
||||
runCatching { dateFormat.parse(string)?.time!! }.getOrNull()
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = CHAPTER_LIST_PREF
|
||||
title = "Split posts into individual chapters"
|
||||
summary = """
|
||||
Instead of showing one 'OneShot' chapter,
|
||||
each post will be it's own chapter
|
||||
""".trimIndent()
|
||||
setDefaultValue(false)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private val SharedPreferences.splitChaptersPref: Boolean
|
||||
get() = getBoolean(CHAPTER_LIST_PREF, false)
|
||||
}
|
||||
|
||||
private const val CHAPTER_LIST_PREF = "prefChapterList"
|
||||
|
@ -0,0 +1,16 @@
|
||||
package eu.kanade.tachiyomi.extension.all.danbooru
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class Pool(
|
||||
val id: Int,
|
||||
@SerialName("updated_at") val updatedAt: String,
|
||||
@SerialName("post_ids") val postIds: List<Int>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Post(
|
||||
@SerialName("file_url") val fileUrl: String,
|
||||
)
|
@ -1,4 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.danbooru
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
internal class FilterTags : Filter.Text("Tags")
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'DeviantArt'
|
||||
extClass = '.DeviantArt'
|
||||
extVersionCode = 6
|
||||
extVersionCode = 7
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -13,6 +12,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
@ -20,8 +20,6 @@ import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.parser.Parser
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@ -32,9 +30,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
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")
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'E-Hentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 24
|
||||
extVersionCode = 25
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.webkit.CookieManager
|
||||
@ -23,6 +22,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
@ -30,8 +30,6 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLEncoder
|
||||
|
||||
abstract class EHentai(
|
||||
@ -41,9 +39,7 @@ abstract class EHentai(
|
||||
|
||||
override val name = "E-Hentai"
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||
private val memberId: String by lazy { getMemberIdPref() }
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Galaxy'
|
||||
extClass = '.GalaxyFactory'
|
||||
extVersionCode = 4
|
||||
extVersionCode = 5
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
|
||||
class GalaxyFactory : SourceFactory {
|
||||
|
||||
@ -22,9 +20,7 @@ class GalaxyFactory : SourceFactory {
|
||||
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
companion object {
|
||||
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
||||
|
@ -4,6 +4,7 @@ ext {
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://grabber.zone'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,8 +1,8 @@
|
||||
ext {
|
||||
extName = '3Hentai'
|
||||
extClass = '.Hentai3Factory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
ext {
|
||||
extName = '3Hentai'
|
||||
extClass = '.Hentai3Factory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,5 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.all.hentai3
|
||||
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user