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
|
```bash
|
||||||
git sparse-checkout set --cone --sparse-index
|
git sparse-checkout set --cone --sparse-index
|
||||||
# add project folders
|
# 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
|
# add a single source
|
||||||
git sparse-checkout add src/<lang>/<source>
|
git sparse-checkout add src/<lang>/<source>
|
||||||
```
|
```
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
2
gradlew
generated
vendored
2
gradlew
generated
vendored
@ -205,7 +205,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# 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.
|
# 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
|
# * 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.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 7
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.bakkin
|
package eu.kanade.tachiyomi.multisrc.bakkin
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@ -33,9 +33,7 @@ abstract class BakkinReaderX(
|
|||||||
"Android ${Build.VERSION.RELEASE}; Mobile) " +
|
"Android ${Build.VERSION.RELEASE}; Mobile) " +
|
||||||
"Tachiyomi/${AppInfo.getVersionName()}"
|
"Tachiyomi/${AppInfo.getVersionName()}"
|
||||||
|
|
||||||
protected val preferences by lazy {
|
protected val preferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json by lazy { Injekt.get<Json>() }
|
private val json by lazy { Injekt.get<Json>() }
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 7
|
baseVersionCode = 9
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:synchrony"))
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@ -46,9 +51,7 @@ abstract class ColaManga(
|
|||||||
|
|
||||||
private val intl = ColaMangaIntl(lang)
|
private val intl = ColaMangaIntl(lang)
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(
|
.rateLimitHost(
|
||||||
@ -150,6 +153,8 @@ abstract class ColaManga(
|
|||||||
protected abstract val genreTitle: String
|
protected abstract val genreTitle: String
|
||||||
protected abstract val statusOngoing: String
|
protected abstract val statusOngoing: String
|
||||||
protected abstract val statusCompleted: 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 {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
title = document.selectFirst("h1.fed-part-eone")!!.text()
|
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 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 {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
name = element.attr("title")
|
name = element.attr("title")
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 8
|
baseVersionCode = 9
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:speedbinb"))
|
api(project(":lib:speedbinb"))
|
||||||
|
@ -28,7 +28,7 @@ open class ComicGamma(
|
|||||||
|
|
||||||
private val json = Injekt.get<Json>()
|
private val json = Injekt.get<Json>()
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(SpeedBinbInterceptor(json))
|
.addInterceptor(SpeedBinbInterceptor(json))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 5
|
baseVersionCode = 6
|
||||||
|
@ -39,7 +39,7 @@ abstract class FansubsCat(
|
|||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
|
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 9
|
baseVersionCode = 10
|
||||||
|
@ -19,6 +19,7 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -246,7 +247,13 @@ abstract class FMReader(
|
|||||||
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
|
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,6 +264,7 @@ abstract class FMReader(
|
|||||||
open val dateWordIndex = 1
|
open val dateWordIndex = 1
|
||||||
|
|
||||||
open fun parseRelativeDate(date: String): Long {
|
open fun parseRelativeDate(date: String): Long {
|
||||||
|
try {
|
||||||
val value = date.split(' ')[dateValueIndex].toInt()
|
val value = date.split(' ')[dateValueIndex].toInt()
|
||||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||||
if (it.contains("(")) {
|
if (it.contains("(")) {
|
||||||
@ -299,13 +307,20 @@ abstract class FMReader(
|
|||||||
set(Calendar.MILLISECOND, 0)
|
set(Calendar.MILLISECOND, 0)
|
||||||
}.timeInMillis
|
}.timeInMillis
|
||||||
else -> {
|
else -> {
|
||||||
return 0
|
return 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return 0L
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun parseAbsoluteDate(dateStr: String): Long {
|
open fun parseAbsoluteDate(dateStr: String): Long {
|
||||||
return runCatching { dateFormat.parse(dateStr)?.time }
|
return try {
|
||||||
.getOrNull() ?: 0L
|
dateFormat.parse(dateStr)?.time ?: 0L
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open val pageListImageSelector = "img.chapter-img"
|
open val pageListImageSelector = "img.chapter-img"
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.foolslide
|
package eu.kanade.tachiyomi.multisrc.foolslide
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@ -273,9 +273,7 @@ abstract class FoolSlide(
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
protected val preferences by lazy {
|
protected val preferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
CheckBoxPreference(screen.context).apply {
|
CheckBoxPreference(screen.context).apply {
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 5
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceScreen
|
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.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -36,8 +36,6 @@ import okhttp3.Response
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@ -56,9 +54,7 @@ abstract class GalleryAdults(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
/* Preferences */
|
/* Preferences */
|
||||||
protected val preferences: SharedPreferences by lazy {
|
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open val useShortTitlePreference = true
|
protected open val useShortTitlePreference = true
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 30
|
baseVersionCode = 31
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.grouple
|
package eu.kanade.tachiyomi.multisrc.grouple
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -22,8 +22,6 @@ import okhttp3.Response
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
@ -37,9 +35,7 @@ abstract class GroupLe(
|
|||||||
final override val lang: String,
|
final override val lang: String,
|
||||||
) : ConfigurableSource, ParsedHttpSource() {
|
) : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 5
|
baseVersionCode = 6
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.guya
|
package eu.kanade.tachiyomi.multisrc.guya
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.preference.ListPreference
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -25,8 +25,6 @@ import org.jsoup.Jsoup
|
|||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
abstract class Guya(
|
abstract class Guya(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -48,9 +46,7 @@ abstract class Guya(
|
|||||||
private val scanlators: ScanlatorStore = ScanlatorStore()
|
private val scanlators: ScanlatorStore = ScanlatorStore()
|
||||||
|
|
||||||
// Preferences configuration
|
// Preferences configuration
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request builder for the "browse" page of the manga
|
// Request builder for the "browse" page of the manga
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 28
|
baseVersionCode = 29
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.heancms
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -26,8 +26,6 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
@ -39,9 +37,7 @@ abstract class HeanCms(
|
|||||||
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
||||||
) : ConfigurableSource, HttpSource() {
|
) : ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.hentaihand
|
package eu.kanade.tachiyomi.multisrc.hentaihand
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.widget.Toast
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
@ -32,8 +32,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -318,9 +316,7 @@ abstract class HentaiHand(
|
|||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
|
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 8
|
||||||
|
@ -93,21 +93,24 @@ class Chapter(
|
|||||||
private val id: Int,
|
private val id: Int,
|
||||||
private val slug: String,
|
private val slug: String,
|
||||||
private val number: JsonPrimitive,
|
private val number: JsonPrimitive,
|
||||||
private val createdBy: Name,
|
|
||||||
private val createdAt: String,
|
private val createdAt: String,
|
||||||
private val chapterStatus: String,
|
private val chapterStatus: String,
|
||||||
private val isAccessible: Boolean,
|
private val isAccessible: Boolean,
|
||||||
|
private val isLocked: Boolean? = false,
|
||||||
|
private val isTimeLocked: Boolean? = false,
|
||||||
private val mangaPost: ChapterPostDetails,
|
private val mangaPost: ChapterPostDetails,
|
||||||
) {
|
) {
|
||||||
fun isPublic() = chapterStatus == "PUBLIC"
|
fun isPublic() = chapterStatus == "PUBLIC"
|
||||||
|
|
||||||
fun isAccessible() = isAccessible
|
fun isAccessible() = isAccessible
|
||||||
|
|
||||||
|
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
|
||||||
|
|
||||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||||
|
val prefix = if (isLocked()) "🔒 " else ""
|
||||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||||
url = "/series/$seriesSlug/$slug#$id"
|
url = "/series/$seriesSlug/$slug#$id"
|
||||||
name = "Chapter $number"
|
name = "${prefix}Chapter $number"
|
||||||
scanlator = createdBy.name
|
|
||||||
date_upload = try {
|
date_upload = try {
|
||||||
dateFormat.parse(createdAt)!!.time
|
dateFormat.parse(createdAt)!!.time
|
||||||
} catch (_: ParseException) {
|
} catch (_: ParseException) {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.iken
|
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.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@ -9,32 +13,34 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.parseAs
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class Iken(
|
abstract class Iken(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
) : HttpSource() {
|
val apiUrl: String = baseUrl,
|
||||||
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
private val json by injectLazy<Json>()
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private var genres = emptyList<Pair<String, String>>()
|
private var genres = emptyList<Pair<String, String>>()
|
||||||
protected val titleCache by lazy {
|
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>()
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
|
||||||
data.posts
|
data.posts
|
||||||
@ -65,7 +71,7 @@ abstract class Iken(
|
|||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
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("page", page.toString())
|
||||||
addQueryParameter("perPage", perPage.toString())
|
addQueryParameter("perPage", perPage.toString())
|
||||||
addQueryParameter("searchTerm", query.trim())
|
addQueryParameter("searchTerm", query.trim())
|
||||||
@ -114,35 +120,76 @@ abstract class Iken(
|
|||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val id = manga.url.substringAfterLast("#")
|
return GET("$baseUrl/series/${manga.url}", headers)
|
||||||
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
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" }
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
return data.post.chapters
|
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) }
|
.map { it.toSChapter(data.post.slug) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
return document.select("main section img").mapIndexed { idx, img ->
|
if (document.selectFirst("svg.lucide-lock") != null) {
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
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) =
|
override fun imageUrlParse(response: Response) =
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
protected fun Document.getNextJson(key: String): String {
|
||||||
json.decodeFromString(body.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 perPage = 18
|
||||||
|
private const val showLockedChapterPrefKey = "pref_show_locked_chapters"
|
||||||
|
private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""")
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 18
|
baseVersionCode = 20
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.kemono
|
package eu.kanade.tachiyomi.multisrc.kemono
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferences
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@ -34,15 +32,14 @@ open class Kemono(
|
|||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource(), ConfigurableSource {
|
||||||
override val supportsLatest = true
|
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()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val preferences =
|
private val preferences = getPreferences()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
|
|
||||||
private val apiPath = "api/v1"
|
private val apiPath = "api/v1"
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 13
|
baseVersionCode = 15
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.keyoapp
|
package eu.kanade.tachiyomi.multisrc.keyoapp
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
@ -23,8 +23,6 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -37,9 +35,7 @@ abstract class Keyoapp(
|
|||||||
final override val lang: String,
|
final override val lang: String,
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
) : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -63,7 +59,8 @@ abstract class Keyoapp(
|
|||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
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 {
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
thumbnail_url = element.getImageUrl("*[style*=background-image]")
|
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.
|
* Get the genres from the search page document.
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 2
|
||||||
|
@ -111,7 +111,11 @@ abstract class LectorMoe(
|
|||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
|
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
|
||||||
val seriesSlug = result.data.slug
|
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 {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Data<T>(val data: T)
|
class Data<T>(val data: T)
|
||||||
@ -53,18 +54,21 @@ class SeriesAuthorDto(
|
|||||||
val name: String,
|
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
|
@Serializable
|
||||||
class SeriesChapterDto(
|
class SeriesChapterDto(
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val number: Float,
|
private val number: Float,
|
||||||
private val createdAt: String,
|
private val releasedAt: String,
|
||||||
|
val subscribersOnly: Boolean,
|
||||||
) {
|
) {
|
||||||
fun toSChapter(seriesSlug: String) = SChapter.create().apply {
|
fun toSChapter(seriesSlug: String) = SChapter.create().apply {
|
||||||
name = "Capítulo ${number.toString().removeSuffix(".0")} - $title"
|
name = "Capítulo ${number.toString().removeSuffix(".0")} - $title"
|
||||||
date_upload = try {
|
date_upload = try {
|
||||||
dateFormat.parse(createdAt)?.time ?: 0L
|
dateFormat.parse(releasedAt)?.time ?: 0L
|
||||||
} catch (_: ParseException) {
|
} catch (_: ParseException) {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 35
|
baseVersionCode = 36
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.multisrc.libgroup
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -57,9 +57,10 @@ abstract class LibGroup(
|
|||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences by getPreferencesLazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
if (getString(SERVER_PREF, "main") == "fourth") {
|
||||||
.migrateOldImageServer()
|
edit().putString(SERVER_PREF, "secondary").apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsLatest = true
|
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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 5
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -30,8 +30,6 @@ import okhttp3.Request
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -51,9 +49,7 @@ abstract class MachineTranslations(
|
|||||||
|
|
||||||
override val lang = language.lang
|
override val lang = language.lang
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
protected val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flag that tracks whether the settings have been changed. It is used to indicate if
|
* A flag that tracks whether the settings have been changed. It is used to indicate if
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 40
|
baseVersionCode = 41
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cryptoaes"))
|
api(project(":lib:cryptoaes"))
|
||||||
|
@ -160,7 +160,7 @@ abstract class Madara(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// exclude/filter bilibili manga from list
|
// 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"
|
open val popularMangaUrlSelector = "div.post-title a"
|
||||||
|
|
||||||
@ -584,7 +584,7 @@ abstract class Madara(
|
|||||||
return MangasPage(entries, hasNextPage)
|
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"
|
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 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 mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
|
||||||
open val mangaDetailsSelectorArtist = "div.artist-content > 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 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 mangaDetailsSelectorThumbnail = "div.summary_image img"
|
||||||
open val mangaDetailsSelectorGenre = "div.genres-content a"
|
open val mangaDetailsSelectorGenre = "div.genres-content a"
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 17
|
baseVersionCode = 18
|
||||||
|
@ -41,6 +41,10 @@ abstract class MadTheme(
|
|||||||
.rateLimit(1, 1, TimeUnit.SECONDS)
|
.rateLimit(1, 1, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
protected open val useLegacyApi = false
|
||||||
|
|
||||||
|
protected open val useSlugSearch = false
|
||||||
|
|
||||||
// TODO: better cookie sharing
|
// TODO: better cookie sharing
|
||||||
// TODO: don't count cached responses against rate limit
|
// TODO: don't count cached responses against rate limit
|
||||||
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
@ -177,59 +181,57 @@ abstract class MadTheme(
|
|||||||
|
|
||||||
val document = response.asJsoup()
|
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)")
|
val script = document.selectFirst("script:containsData(bookId)")
|
||||||
?: throw Exception("Cannot find script")
|
?: throw Exception("Cannot find script")
|
||||||
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
|
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
|
||||||
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
|
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
|
||||||
|
|
||||||
// Use slug search by default
|
var chaptersList = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
|
|
||||||
if (!slugRequest.isSuccessful) {
|
val fetchApi = document.selectFirst("div#show-more-chapters > span")
|
||||||
throw Exception("HTTP error ${slugRequest.code}")
|
?.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())
|
return chaptersList
|
||||||
|
|
||||||
if (totalChapters != null && finalDocument.size < totalChapters) {
|
|
||||||
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
|
|
||||||
finalDocument = idRequest.asJsoup().select(chapterListSelector())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalDocument.map {
|
private fun buildChapterUrl(mangaId: String, mangaSlug: String): HttpUrl {
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
|
|
||||||
return baseUrl.toHttpUrl().newBuilder().apply {
|
return baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addPathSegment("api")
|
addPathSegment("api")
|
||||||
addPathSegment("manga")
|
addPathSegment("manga")
|
||||||
addPathSegment(fetchByParam)
|
addPathSegment(if (useSlugSearch) mangaSlug else mangaId)
|
||||||
addPathSegment("chapters")
|
addPathSegment("chapters")
|
||||||
addQueryParameter("source", "detail")
|
addQueryParameter("source", "detail")
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request =
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
|
if (useLegacyApi) {
|
||||||
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
val mangaId = MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)
|
||||||
.addQueryParameter("manga_id", mangaId)
|
val url = mangaId?.let {
|
||||||
|
"$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("manga_id", it)
|
||||||
.addQueryParameter("manga_name", manga.title)
|
.addQueryParameter("manga_name", manga.title)
|
||||||
.fragment("idFound")
|
.fragment("idFound")
|
||||||
.build()
|
.build()
|
||||||
|
.toString()
|
||||||
|
} ?: (baseUrl + manga.url)
|
||||||
|
|
||||||
GET(url, headers)
|
return GET(url, headers)
|
||||||
} ?: GET("$baseUrl${manga.url}", headers)
|
}
|
||||||
|
return GET(baseUrl + manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
if (genresList == null) {
|
if (genresList == null) {
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 5
|
baseVersionCode = 6
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -9,42 +13,144 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
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(
|
abstract class MangaBox(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
private val mirrorEntries: Array<String>,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH),
|
private val dateFormat: SimpleDateFormat = SimpleDateFormat(
|
||||||
) : ParsedHttpSource() {
|
"MMM-dd-yyyy HH:mm",
|
||||||
|
Locale.ENGLISH,
|
||||||
|
).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
},
|
||||||
|
) : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val baseUrl: String get() = mirror
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.connectTimeout(15, TimeUnit.SECONDS)
|
.addInterceptor(::useAltCdnInterceptor)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
.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()
|
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"
|
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
||||||
|
|
||||||
@ -58,10 +164,11 @@ abstract class MangaBox(
|
|||||||
return GET("$baseUrl/$latestUrlPath$page", headers)
|
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 {
|
return SManga.create().apply {
|
||||||
element.select(urlSelector).first()!!.let {
|
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()
|
title = it.text()
|
||||||
}
|
}
|
||||||
thumbnail_url = element.select("img").first()!!.attr("abs:src")
|
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 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 latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
return if (query.isNotBlank()) {
|
||||||
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
|
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment(normalizeSearchQuery(query))
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
} else {
|
} else {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder()
|
val url = "$baseUrl/genre".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())
|
url.addQueryParameter("page", page.toString())
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
||||||
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
|
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
GET(url.build(), headers)
|
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 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 mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
|
||||||
|
|
||||||
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
|
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 {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
if (manga.url.startsWith("http")) {
|
if (manga.url.startsWith("http")) {
|
||||||
@ -146,11 +238,15 @@ abstract class MangaBox(
|
|||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
||||||
title = infoElement.select("h1, h2").first()!!.text()
|
title = infoElement.select("h1, h2").first()!!.text()
|
||||||
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a").eachText().joinToString()
|
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a")
|
||||||
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
|
.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()
|
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
|
||||||
?.select("a")?.joinToString { it.text() } // kakalot
|
?.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)
|
} ?: checkForRedirectMessage(document)
|
||||||
description = document.select(descriptionSelector).firstOrNull()?.ownText()
|
description = document.select(descriptionSelector).firstOrNull()?.ownText()
|
||||||
?.replace("""^$title summary:\s""".toRegex(), "")
|
?.replace("""^$title summary:\s""".toRegex(), "")
|
||||||
@ -201,42 +297,21 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
private fun Element.selectDateFromElement(): Element {
|
private fun Element.selectDateFromElement(): Element {
|
||||||
val defaultChapterDateSelector = "span"
|
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 {
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
return SChapter.create().apply {
|
return SChapter.create().apply {
|
||||||
element.select("a").let {
|
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()
|
name = it.text()
|
||||||
scanlator =
|
scanlator =
|
||||||
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
|
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
|
||||||
}
|
}
|
||||||
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
|
date_upload = dateFormat.tryParse(element.selectDateFromElement().attr("title"))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,26 +322,59 @@ abstract class MangaBox(
|
|||||||
return super.pageListRequest(chapter)
|
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> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select(pageListSelector)
|
val element = document.select("head > script").lastOrNull()
|
||||||
// filter out bad elements for mangakakalots
|
?: return emptyList()
|
||||||
.filterNot { it.attr("src").endsWith("log") }
|
val cdns =
|
||||||
.mapIndexed { i, element ->
|
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
|
||||||
val url = element.attr("abs:src").let { src ->
|
val chapterImages = extractArray(element.html(), "chapterImages")
|
||||||
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
|
|
||||||
"https://images.weserv.nl/?url=" + src.substringAfter("//")
|
// Add all parsed cdns to set
|
||||||
} else {
|
cdnSet.addAll(cdns)
|
||||||
src
|
|
||||||
|
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(), url)
|
Page(i, document.location(), parsedUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
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()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
@ -282,46 +390,27 @@ abstract class MangaBox(
|
|||||||
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
||||||
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
||||||
str = str.replace("đ".toRegex(), "d")
|
str = str.replace("đ".toRegex(), "d")
|
||||||
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
|
str = str.replace(
|
||||||
|
"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(),
|
||||||
|
"_",
|
||||||
|
)
|
||||||
str = str.replace("_+_".toRegex(), "_")
|
str = str.replace("_+_".toRegex(), "_")
|
||||||
str = str.replace("""^_+|_+$""".toRegex(), "")
|
str = str.replace("""^_+|_+$""".toRegex(), "")
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
|
override fun getFilterList() = FilterList(
|
||||||
FilterList(
|
|
||||||
KeywordFilter(getKeywordFilters()),
|
|
||||||
SortFilter(getSortFilters()),
|
|
||||||
StatusFilter(getStatusFilters()),
|
|
||||||
AdvGenreFilter(getAdvancedGenreFilters()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
FilterList(
|
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
SortFilter(getSortFilters()),
|
SortFilter(getSortFilters()),
|
||||||
StatusFilter(getStatusFilters()),
|
StatusFilter(getStatusFilters()),
|
||||||
GenreFilter(getGenreFilters()),
|
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 SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
|
||||||
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||||
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", 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(
|
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||||
Pair("latest", "Latest"),
|
Pair("latest", "Latest"),
|
||||||
Pair("newest", "Newest"),
|
Pair("newest", "Newest"),
|
||||||
@ -337,53 +426,72 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||||
Pair("all", "ALL"),
|
Pair("all", "ALL"),
|
||||||
Pair("2", "Action"),
|
Pair("action", "Action"),
|
||||||
Pair("3", "Adult"),
|
Pair("adult", "Adult"),
|
||||||
Pair("4", "Adventure"),
|
Pair("adventure", "Adventure"),
|
||||||
Pair("6", "Comedy"),
|
Pair("comedy", "Comedy"),
|
||||||
Pair("7", "Cooking"),
|
Pair("cooking", "Cooking"),
|
||||||
Pair("9", "Doujinshi"),
|
Pair("doujinshi", "Doujinshi"),
|
||||||
Pair("10", "Drama"),
|
Pair("drama", "Drama"),
|
||||||
Pair("11", "Ecchi"),
|
Pair("ecchi", "Ecchi"),
|
||||||
Pair("12", "Fantasy"),
|
Pair("fantasy", "Fantasy"),
|
||||||
Pair("13", "Gender bender"),
|
Pair("gender-bender", "Gender bender"),
|
||||||
Pair("14", "Harem"),
|
Pair("harem", "Harem"),
|
||||||
Pair("15", "Historical"),
|
Pair("historical", "Historical"),
|
||||||
Pair("16", "Horror"),
|
Pair("horror", "Horror"),
|
||||||
Pair("45", "Isekai"),
|
Pair("isekai", "Isekai"),
|
||||||
Pair("17", "Josei"),
|
Pair("josei", "Josei"),
|
||||||
Pair("44", "Manhua"),
|
Pair("manhua", "Manhua"),
|
||||||
Pair("43", "Manhwa"),
|
Pair("manhwa", "Manhwa"),
|
||||||
Pair("19", "Martial arts"),
|
Pair("martial-arts", "Martial arts"),
|
||||||
Pair("20", "Mature"),
|
Pair("mature", "Mature"),
|
||||||
Pair("21", "Mecha"),
|
Pair("mecha", "Mecha"),
|
||||||
Pair("22", "Medical"),
|
Pair("medical", "Medical"),
|
||||||
Pair("24", "Mystery"),
|
Pair("mystery", "Mystery"),
|
||||||
Pair("25", "One shot"),
|
Pair("one-shot", "One shot"),
|
||||||
Pair("26", "Psychological"),
|
Pair("psychological", "Psychological"),
|
||||||
Pair("27", "Romance"),
|
Pair("romance", "Romance"),
|
||||||
Pair("28", "School life"),
|
Pair("school-life", "School life"),
|
||||||
Pair("29", "Sci fi"),
|
Pair("sci-fi", "Sci fi"),
|
||||||
Pair("30", "Seinen"),
|
Pair("seinen", "Seinen"),
|
||||||
Pair("31", "Shoujo"),
|
Pair("shoujo", "Shoujo"),
|
||||||
Pair("32", "Shoujo ai"),
|
Pair("shoujo-ai", "Shoujo ai"),
|
||||||
Pair("33", "Shounen"),
|
Pair("shounen", "Shounen"),
|
||||||
Pair("34", "Shounen ai"),
|
Pair("shounen-ai", "Shounen ai"),
|
||||||
Pair("35", "Slice of life"),
|
Pair("slice-of-life", "Slice of life"),
|
||||||
Pair("36", "Smut"),
|
Pair("smut", "Smut"),
|
||||||
Pair("37", "Sports"),
|
Pair("sports", "Sports"),
|
||||||
Pair("38", "Supernatural"),
|
Pair("supernatural", "Supernatural"),
|
||||||
Pair("39", "Tragedy"),
|
Pair("tragedy", "Tragedy"),
|
||||||
Pair("40", "Webtoons"),
|
Pair("webtoons", "Webtoons"),
|
||||||
Pair("41", "Yaoi"),
|
Pair("yaoi", "Yaoi"),
|
||||||
Pair("42", "Yuri"),
|
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>>) :
|
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||||
fun toUriPart() = vals[state].first
|
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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -48,7 +48,7 @@ abstract class MangaEsp(
|
|||||||
|
|
||||||
protected open val useApiSearch = false
|
protected open val useApiSearch = false
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 29
|
baseVersionCode = 30
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:randomua"))
|
api(project(":lib:randomua"))
|
||||||
|
@ -17,7 +17,6 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
@ -31,7 +30,7 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.io.IOException
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -49,6 +48,7 @@ abstract class MangaHub(
|
|||||||
|
|
||||||
private var baseApiUrl = "https://api.mghcdn.com"
|
private var baseApiUrl = "https://api.mghcdn.com"
|
||||||
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
||||||
|
private val regex = Regex("mhub_access=([^;]+)")
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.setRandomUserAgent(
|
.setRandomUserAgent(
|
||||||
@ -91,8 +91,6 @@ abstract class MangaHub(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshApiKey(chapter: SChapter) {
|
private fun refreshApiKey(chapter: SChapter) {
|
||||||
val now = Calendar.getInstance().time.time
|
|
||||||
|
|
||||||
val slug = "$baseUrl${chapter.url}"
|
val slug = "$baseUrl${chapter.url}"
|
||||||
.toHttpUrlOrNull()
|
.toHttpUrlOrNull()
|
||||||
?.pathSegments
|
?.pathSegments
|
||||||
@ -104,32 +102,28 @@ abstract class MangaHub(
|
|||||||
baseUrl.toHttpUrl()
|
baseUrl.toHttpUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val oldKey = client.cookieJar
|
||||||
|
.loadForRequest(baseUrl.toHttpUrl())
|
||||||
|
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
||||||
|
|
||||||
|
for (i in 1..2) {
|
||||||
// Clear key cookie
|
// Clear key cookie
|
||||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
||||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
||||||
|
|
||||||
// Set required cookie (for cache busting?)
|
// We try requesting again with param if the first one fails
|
||||||
val recently = buildJsonObject {
|
val query = if (i == 2) "?reloadKey=1" else ""
|
||||||
putJsonObject((now - (0..3600).random()).toString()) {
|
|
||||||
put("mangaID", (1..42_000).random())
|
try {
|
||||||
put("number", (1..20).random())
|
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(
|
data class SMangaDTO(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
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.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@ -16,8 +16,6 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -34,14 +32,12 @@ abstract class MangaThemesiaAlt(
|
|||||||
protected open val listUrl = "$mangaUrlDirectory/list-mode/"
|
protected open val listUrl = "$mangaUrlDirectory/list-mode/"
|
||||||
protected open val listSelector = "div#content div.soralist ul li a.series"
|
protected open val listSelector = "div#content div.soralist ul li a.series"
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
protected val preferences by getPreferencesLazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).also {
|
if (contains("__random_part_cache")) {
|
||||||
if (it.contains("__random_part_cache")) {
|
edit().remove("__random_part_cache").apply()
|
||||||
it.edit().remove("__random_part_cache").apply()
|
|
||||||
}
|
|
||||||
if (it.contains("titles_without_random_part")) {
|
|
||||||
it.edit().remove("titles_without_random_part").apply()
|
|
||||||
}
|
}
|
||||||
|
if (contains("titles_without_random_part")) {
|
||||||
|
edit().remove("titles_without_random_part").apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 7
|
||||||
|
@ -33,7 +33,7 @@ open class MCCMS(
|
|||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
override val client by lazy {
|
override val client by lazy {
|
||||||
network.client.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ open class MCCMSWeb(
|
|||||||
override val supportsLatest get() = true
|
override val supportsLatest get() = true
|
||||||
|
|
||||||
override val client by lazy {
|
override val client by lazy {
|
||||||
network.client.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
@ -26,7 +26,7 @@ abstract class MultiChan(
|
|||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 4
|
||||||
|
@ -40,7 +40,7 @@ abstract class Senkuro(
|
|||||||
.add("Content-Type", "application/json")
|
.add("Content-Type", "application/json")
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
override val client: OkHttpClient =
|
||||||
network.client.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.rateLimit(3)
|
.rateLimit(3)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 11
|
baseVersionCode = 12
|
||||||
|
@ -34,7 +34,7 @@ abstract class SinMH(
|
|||||||
protected open val mobileUrl = _baseUrl.replaceFirst("www.", "m.")
|
protected open val mobileUrl = _baseUrl.replaceFirst("www.", "m.")
|
||||||
override val supportsLatest = true
|
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()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("User-Agent", System.getProperty("http.agent")!!)
|
.add("User-Agent", System.getProperty("http.agent")!!)
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 4
|
||||||
|
@ -45,7 +45,7 @@ open class Webtoons(
|
|||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.cookieJar(
|
.cookieJar(
|
||||||
object : CookieJar {
|
object : CookieJar {
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
||||||
@ -197,7 +197,7 @@ open class Webtoons(
|
|||||||
open fun parseDetailsThumbnail(document: Document): String? {
|
open fun parseDetailsThumbnail(document: Document): String? {
|
||||||
val picElement = document.select("#content > div.cont_box > div.detail_body")
|
val picElement = document.select("#content > div.cont_box > div.detail_body")
|
||||||
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
|
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") }
|
.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">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".pt.blackscans.BlackScansUrlActivity"
|
android:name="eu.kanade.tachiyomi.multisrc.yuyu.YuYuUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
@ -11,11 +11,10 @@
|
|||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="blackscans.site"
|
android:host="${SOURCEHOST}"
|
||||||
android:pathPattern="/series/..*"
|
android:pathPattern="/..*"
|
||||||
android:scheme="https" />
|
android:scheme="${SOURCESCHEME}" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</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.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@ -7,7 +7,7 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class YushukeMangasUrlActivity : Activity() {
|
class YuYuUrlActivity : Activity() {
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
private val tag = javaClass.simpleName
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class YushukeMangasUrlActivity : Activity() {
|
|||||||
if (pathSegment != null && pathSegment.size > 1) {
|
if (pathSegment != null && pathSegment.size > 1) {
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
|
putExtra("query", "${YuYu.PREFIX_SEARCH}${pathSegment[1]}")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Akuma'
|
extName = 'Akuma'
|
||||||
extClass = '.AkumaFactory'
|
extClass = '.AkumaFactory'
|
||||||
extVersionCode = 5
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.akuma
|
package eu.kanade.tachiyomi.extension.all.akuma
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
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.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
@ -25,8 +25,6 @@ import okhttp3.Response
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -48,12 +46,12 @@ class Akuma(
|
|||||||
|
|
||||||
private var storedToken: String? = null
|
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 {
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}
|
}
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(ddosGuardIntercept)
|
.addInterceptor(ddosGuardIntercept)
|
||||||
.addInterceptor(::tokenInterceptor)
|
.addInterceptor(::tokenInterceptor)
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
@ -112,9 +110,7 @@ class Akuma(
|
|||||||
return storedToken!!
|
return storedToken!!
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val displayFullTitle: Boolean get() = preferences.getBoolean(PREF_TITLE, false)
|
private val displayFullTitle: Boolean get() = preferences.getBoolean(PREF_TITLE, false)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 48
|
extVersionCode = 49
|
||||||
isNsfw = true
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -48,10 +49,7 @@ open class BatoTo(
|
|||||||
private val siteLang: String,
|
private val siteLang: String,
|
||||||
) : ConfigurableSource, ParsedHttpSource() {
|
) : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences by getPreferencesLazy { migrateMirrorPref() }
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
.migrateMirrorPref()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name: String = "Bato.to"
|
override val name: String = "Bato.to"
|
||||||
override val baseUrl: String get() = mirror
|
override val baseUrl: String get() = mirror
|
||||||
@ -125,14 +123,12 @@ open class BatoTo(
|
|||||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
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)!!
|
val selectedMirror = getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)!!
|
||||||
|
|
||||||
if (selectedMirror in DEPRECATED_MIRRORS) {
|
if (selectedMirror in DEPRECATED_MIRRORS) {
|
||||||
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
|
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Comic Fury'
|
extName = 'Comic Fury'
|
||||||
extClass = '.ComicFuryFactory'
|
extClass = '.ComicFuryFactory'
|
||||||
extVersionCode = 3
|
extVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comicfury
|
package eu.kanade.tachiyomi.extension.all.comicfury
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@ -35,7 +33,7 @@ class ComicFury(
|
|||||||
override val name: String = "Comic Fury$extraName" // Used for No Text
|
override val name: String = "Comic Fury$extraName" // Used for No Text
|
||||||
override val supportsLatest: Boolean = true
|
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
|
* 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 }
|
private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
|
||||||
|
|
||||||
// START OF AUTHOR NOTES //
|
// START OF AUTHOR NOTES //
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ ext {
|
|||||||
themePkg = 'gigaviewer'
|
themePkg = 'gigaviewer'
|
||||||
baseUrl = 'https://comic-growl.com'
|
baseUrl = 'https://comic-growl.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
ignored_groups_title=Ignored Groups
|
ignored_groups_title=Ignored Groups
|
||||||
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
||||||
|
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_title=Include Tags
|
||||||
include_tags_on=More specific, but might contain spoilers!
|
include_tags_on=More specific, but might contain spoilers!
|
||||||
include_tags_off=Only the broader genres
|
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_title=Update Covers
|
||||||
update_cover_on=Keep cover updated
|
update_cover_on=Keep cover updated
|
||||||
update_cover_off=Prefer first cover
|
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_title=Score Position in the Description
|
||||||
score_position_top=Top
|
score_position_top=Top
|
||||||
score_position_middle=Middle
|
score_position_middle=Middle
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
ignored_groups_title=Grupos Ignorados
|
ignored_groups_title=Grupos Ignorados
|
||||||
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
|
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_title=Incluir Tags
|
||||||
include_tags_on=Mais detalhadas, mas podem conter spoilers
|
include_tags_on=Mais detalhadas, mas podem conter spoilers
|
||||||
include_tags_off=Apenas os gêneros básicos
|
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_title=Atualizar Capas
|
||||||
update_cover_on=Manter capas atualizadas
|
update_cover_on=Manter capas atualizadas
|
||||||
update_cover_off=Usar apenas a primeira capa
|
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_title=Posição da Nota na Descrição
|
||||||
score_position_top=Topo
|
score_position_top=Topo
|
||||||
score_position_middle=Meio
|
score_position_middle=Meio
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
extClass = '.ComickFactory'
|
extClass = '.ComickFactory'
|
||||||
extVersionCode = 52
|
extVersionCode = 55
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.ListPreference
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Builder
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -65,10 +64,7 @@ abstract class Comick(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
.newLineIgnoredGroups()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
EditTextPreference(screen.context).apply {
|
EditTextPreference(screen.context).apply {
|
||||||
@ -83,6 +79,20 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.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 {
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = INCLUDE_MU_TAGS_PREF
|
key = INCLUDE_MU_TAGS_PREF
|
||||||
title = intl["include_tags_title"]
|
title = intl["include_tags_title"]
|
||||||
@ -125,6 +135,20 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.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 {
|
ListPreference(screen.context).apply {
|
||||||
key = SCORE_POSITION_PREF
|
key = SCORE_POSITION_PREF
|
||||||
title = intl["score_position_title"]
|
title = intl["score_position_title"]
|
||||||
@ -160,6 +184,9 @@ abstract class Comick(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
|
private val SharedPreferences.showAlternativeTitles: Boolean
|
||||||
|
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
||||||
|
|
||||||
private val SharedPreferences.includeMuTags: Boolean
|
private val SharedPreferences.includeMuTags: Boolean
|
||||||
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
|
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
|
||||||
|
|
||||||
@ -169,6 +196,17 @@ abstract class Comick(
|
|||||||
private val SharedPreferences.updateCover: Boolean
|
private val SharedPreferences.updateCover: Boolean
|
||||||
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
|
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
|
private val SharedPreferences.scorePosition: String
|
||||||
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
|
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
|
||||||
|
|
||||||
@ -277,6 +315,16 @@ abstract class Comick(
|
|||||||
return MangasPage(entries, end < searchResponse.size)
|
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
|
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
|
||||||
filters.forEach { it ->
|
filters.forEach { it ->
|
||||||
@ -298,7 +346,7 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is DemographicFilter -> {
|
is DemographicFilter -> {
|
||||||
it.state.filter { it.isIncluded() }.forEach {
|
it.state.filter { it.state }.forEach {
|
||||||
addQueryParameter("demographic", it.value)
|
addQueryParameter("demographic", it.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,6 +367,12 @@ abstract class Comick(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ContentRatingFilter -> {
|
||||||
|
if (it.state > 0) {
|
||||||
|
addQueryParameter("content_rating", it.getValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is CreatedAtFilter -> {
|
is CreatedAtFilter -> {
|
||||||
if (it.state > 0) {
|
if (it.state > 0) {
|
||||||
addQueryParameter("time", it.getValue())
|
addQueryParameter("time", it.getValue())
|
||||||
@ -345,14 +399,14 @@ abstract class Comick(
|
|||||||
|
|
||||||
is TagFilter -> {
|
is TagFilter -> {
|
||||||
if (it.state.isNotEmpty()) {
|
if (it.state.isNotEmpty()) {
|
||||||
it.state.split(",").forEach {
|
addTagQueryParameters(this, it.state, "tags")
|
||||||
addQueryParameter(
|
|
||||||
"tags",
|
|
||||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
|
||||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ExcludedTagFilter -> {
|
||||||
|
if (it.state.isNotEmpty()) {
|
||||||
|
addTagQueryParameters(this, it.state, "excluded-tags")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
@ -405,14 +459,18 @@ abstract class Comick(
|
|||||||
return mangaData.toSManga(
|
return mangaData.toSManga(
|
||||||
includeMuTags = preferences.includeMuTags,
|
includeMuTags = preferences.includeMuTags,
|
||||||
scorePosition = preferences.scorePosition,
|
scorePosition = preferences.scorePosition,
|
||||||
|
showAlternativeTitles = preferences.showAlternativeTitles,
|
||||||
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
||||||
groupTags = preferences.groupTags,
|
groupTags = preferences.groupTags,
|
||||||
|
titleLang = preferences.localTitle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return mangaData.toSManga(
|
return mangaData.toSManga(
|
||||||
includeMuTags = preferences.includeMuTags,
|
includeMuTags = preferences.includeMuTags,
|
||||||
scorePosition = preferences.scorePosition,
|
scorePosition = preferences.scorePosition,
|
||||||
|
showAlternativeTitles = preferences.showAlternativeTitles,
|
||||||
groupTags = preferences.groupTags,
|
groupTags = preferences.groupTags,
|
||||||
|
titleLang = preferences.localTitle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,8 +565,9 @@ abstract class Comick(
|
|||||||
|
|
||||||
override fun getFilterList() = getFilters()
|
override fun getFilterList() = getFilters()
|
||||||
|
|
||||||
private fun SharedPreferences.newLineIgnoredGroups(): SharedPreferences {
|
private fun SharedPreferences.newLineIgnoredGroups() {
|
||||||
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return this
|
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
|
||||||
|
|
||||||
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
|
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
|
||||||
|
|
||||||
edit()
|
edit()
|
||||||
@ -522,14 +581,14 @@ abstract class Comick(
|
|||||||
)
|
)
|
||||||
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
|
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||||
|
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
|
||||||
|
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
|
||||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||||
private const val GROUP_TAGS_PREF = "GroupTags"
|
private const val GROUP_TAGS_PREF = "GroupTags"
|
||||||
@ -539,6 +598,8 @@ abstract class Comick(
|
|||||||
private const val FIRST_COVER_DEFAULT = true
|
private const val FIRST_COVER_DEFAULT = true
|
||||||
private const val SCORE_POSITION_PREF = "ScorePosition"
|
private const val SCORE_POSITION_PREF = "ScorePosition"
|
||||||
const val SCORE_POSITION_DEFAULT = "top"
|
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 LIMIT = 20
|
||||||
private const val CHAPTERS_LIMIT = 99999
|
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.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.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.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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
@ -36,13 +37,20 @@ class Manga(
|
|||||||
fun toSManga(
|
fun toSManga(
|
||||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
||||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
scorePosition: String = SCORE_POSITION_DEFAULT,
|
||||||
|
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
|
||||||
covers: List<MDcovers>? = null,
|
covers: List<MDcovers>? = null,
|
||||||
groupTags: Boolean = GROUP_TAGS_DEFAULT,
|
groupTags: Boolean = GROUP_TAGS_DEFAULT,
|
||||||
) =
|
titleLang: String,
|
||||||
SManga.create().apply {
|
): 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
|
// appennding # at end as part of migration from slug to hid
|
||||||
url = "/comic/${comic.hid}#"
|
url = "/comic/${comic.hid}#"
|
||||||
title = comic.title
|
title = entryTitle
|
||||||
description = buildString {
|
description = buildString {
|
||||||
if (scorePosition == "top") append(comic.fancyScore)
|
if (scorePosition == "top") append(comic.fancyScore)
|
||||||
val desc = comic.desc?.beautifyDescription()
|
val desc = comic.desc?.beautifyDescription()
|
||||||
@ -54,11 +62,12 @@ class Manga(
|
|||||||
if (this.isNotEmpty()) append("\n\n")
|
if (this.isNotEmpty()) append("\n\n")
|
||||||
append(comic.fancyScore)
|
append(comic.fancyScore)
|
||||||
}
|
}
|
||||||
if (comic.altTitles.isNotEmpty()) {
|
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
|
||||||
if (this.isNotEmpty()) append("\n\n")
|
if (this.isNotEmpty()) append("\n\n")
|
||||||
append("Alternative Titles:\n")
|
append("Alternative Titles:\n")
|
||||||
append(
|
append(
|
||||||
comic.altTitles.mapNotNull { title ->
|
titles.distinctBy { it.title }.filter { it.title != entryTitle }
|
||||||
|
.mapNotNull { title ->
|
||||||
title.title?.let { "• $it" }
|
title.title?.let { "• $it" }
|
||||||
}.joinToString("\n"),
|
}.joinToString("\n"),
|
||||||
)
|
)
|
||||||
@ -95,6 +104,7 @@ class Manga(
|
|||||||
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
||||||
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -169,6 +179,7 @@ class MDcovers(
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Title(
|
class Title(
|
||||||
val title: String?,
|
val title: String?,
|
||||||
|
val lang: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -11,6 +11,7 @@ fun getFilters(): FilterList {
|
|||||||
TypeFilter("Type", getTypeList),
|
TypeFilter("Type", getTypeList),
|
||||||
SortFilter("Sort", getSortsList),
|
SortFilter("Sort", getSortsList),
|
||||||
StatusFilter("Status", getStatusList),
|
StatusFilter("Status", getStatusList),
|
||||||
|
ContentRatingFilter("Content Rating", getContentRatingList),
|
||||||
CompletedFilter("Completely Scanlated?"),
|
CompletedFilter("Completely Scanlated?"),
|
||||||
CreatedAtFilter("Created at", getCreatedAtList),
|
CreatedAtFilter("Created at", getCreatedAtList),
|
||||||
MinimumFilter("Minimum Chapters"),
|
MinimumFilter("Minimum Chapters"),
|
||||||
@ -20,6 +21,7 @@ fun getFilters(): FilterList {
|
|||||||
ToYearFilter("To"),
|
ToYearFilter("To"),
|
||||||
Filter.Header("Separate tags with commas"),
|
Filter.Header("Separate tags with commas"),
|
||||||
TagFilter("Tags"),
|
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 TagFilter(name: String) : TextFilter(name)
|
||||||
|
|
||||||
|
internal class ExcludedTagFilter(name: String) : TextFilter(name)
|
||||||
|
|
||||||
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
|
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>>) :
|
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
|
||||||
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
|
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) :
|
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
||||||
SelectFilter(name, statusList, state)
|
SelectFilter(name, statusList, state)
|
||||||
|
|
||||||
|
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
||||||
|
SelectFilter(name, statusList, state)
|
||||||
|
|
||||||
/** Generics **/
|
/** Generics **/
|
||||||
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
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("Shoujo", "2"),
|
||||||
Pair("Seinen", "3"),
|
Pair("Seinen", "3"),
|
||||||
Pair("Josei", "4"),
|
Pair("Josei", "4"),
|
||||||
|
Pair("None", "5"),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val getTypeList: List<Pair<String, String>> = listOf(
|
private val getTypeList: List<Pair<String, String>> = listOf(
|
||||||
Pair("Manga", "jp"),
|
Pair("Manga", "jp"),
|
||||||
Pair("Manhwa", "kr"),
|
Pair("Manhwa", "kr"),
|
||||||
Pair("Manhua", "cn"),
|
Pair("Manhua", "cn"),
|
||||||
|
Pair("Others", "others"),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val getCreatedAtList: List<Pair<String, String>> = listOf(
|
private val getCreatedAtList: List<Pair<String, String>> = listOf(
|
||||||
@ -190,3 +199,10 @@ private val getStatusList: List<Pair<String, String>> = listOf(
|
|||||||
Pair("Cancelled", "3"),
|
Pair("Cancelled", "3"),
|
||||||
Pair("Hiatus", "4"),
|
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 {
|
ext {
|
||||||
extName = 'Comico'
|
extName = 'Comico'
|
||||||
extClass = '.ComicoFactory'
|
extClass = '.ComicoFactory'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ open class Comico(
|
|||||||
this["Origin"] = baseUrl
|
this["Origin"] = baseUrl
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.cookieJar(
|
.cookieJar(
|
||||||
object : CookieJar {
|
object : CookieJar {
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'ComicsKingdom'
|
extName = 'ComicsKingdom'
|
||||||
extClass = '.ComicsKingdomFactory'
|
extClass = '.ComicsKingdomFactory'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comicskingdom
|
package eu.kanade.tachiyomi.extension.all.comicskingdom
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@ -19,8 +19,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -301,9 +299,7 @@ class ComicsKingdom(override val lang: String) : ConfigurableSource, HttpSource(
|
|||||||
screen.addPreference(compactpref)
|
screen.addPreference(compactpref)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldCompact() = preferences.getBoolean("compactPref", true)
|
private fun shouldCompact() = preferences.getBoolean("compactPref", true)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = "Comikey"
|
extName = "Comikey"
|
||||||
extClass = ".ComikeyFactory"
|
extClass = ".ComikeyFactory"
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@ -75,9 +76,7 @@ open class Comikey(
|
|||||||
classLoader = this::class.java.classLoader!!,
|
classLoader = this::class.java.classLoader!!,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Danbooru'
|
extName = 'Danbooru'
|
||||||
extClass = '.Danbooru'
|
extClass = '.Danbooru'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,51 +1,46 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.danbooru
|
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.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.decodeFromString
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import kotlinx.serialization.json.JsonObject
|
import keiyoushi.utils.parseAs
|
||||||
import kotlinx.serialization.json.jsonArray
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class Danbooru : ParsedHttpSource() {
|
class Danbooru : HttpSource(), ConfigurableSource {
|
||||||
override val name: String = "Danbooru"
|
override val name: String = "Danbooru"
|
||||||
override val baseUrl: String = "https://danbooru.donmai.us"
|
override val baseUrl: String = "https://danbooru.donmai.us"
|
||||||
override val lang: String = "all"
|
override val lang: String = "all"
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat by lazy {
|
private val dateFormat =
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
|
||||||
}
|
|
||||||
|
private val preference by getPreferencesLazy()
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
searchMangaRequest(page, "", FilterList())
|
searchMangaRequest(page, "", FilterList())
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga =
|
override fun popularMangaParse(response: Response) =
|
||||||
searchMangaFromElement(element)
|
searchMangaParse(response)
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String =
|
|
||||||
searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String =
|
|
||||||
searchMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
|
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
|
||||||
@ -87,86 +82,100 @@ class Danbooru : ParsedHttpSource() {
|
|||||||
return GET(url.build(), headers)
|
return GET(url.build(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector(): String =
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
"article.post-preview"
|
val document = response.asJsoup()
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
val entries = document.select("article.post-preview").map {
|
||||||
url = element.selectFirst(".post-preview-link")?.attr("href")!!
|
searchMangaFromElement(it)
|
||||||
title = element.selectFirst("div.text-center")?.text() ?: ""
|
}
|
||||||
|
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")
|
thumbnail_url = element.selectFirst("source")?.attr("srcset")
|
||||||
?.substringAfterLast(',')?.trim()
|
?.substringAfterLast(',')?.trim()
|
||||||
?.substringBeforeLast(' ')?.trimStart()
|
?.substringBeforeLast(' ')?.trimStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String =
|
|
||||||
"a.paginator-next"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
searchMangaRequest(page, "", FilterList(FilterOrder("created_at")))
|
searchMangaRequest(page, "", FilterList(FilterOrder("created_at")))
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
searchMangaSelector()
|
searchMangaParse(response)
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||||
searchMangaFromElement(element)
|
val document = response.asJsoup()
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String =
|
|
||||||
searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(document.location())
|
setUrlWithoutDomain(document.location())
|
||||||
|
title = document.selectFirst(".pool-category-series, .pool-category-collection")!!.text()
|
||||||
title = document.selectFirst(".pool-category-series, .pool-category-collection")?.text() ?: ""
|
description = document.getElementById("description")?.wholeText()
|
||||||
description = document.getElementById("description")?.wholeText() ?: ""
|
author = document.selectFirst("#description a[href*=artists]")?.ownText()
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
artist = author
|
||||||
|
update_strategy = if (!preference.splitChaptersPref) {
|
||||||
|
UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
} else {
|
||||||
|
UpdateStrategy.ALWAYS_UPDATE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request =
|
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(
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val data = response.parseAs<Pool>()
|
||||||
|
|
||||||
|
return if (preference.splitChaptersPref) {
|
||||||
|
data.postIds.mapIndexed { index, id ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
val data = json.decodeFromString<JsonObject>(response.body.string())
|
url = "/posts/$id"
|
||||||
|
name = "Post ${index + 1}"
|
||||||
val id = data["id"]!!.jsonPrimitive.content
|
chapter_number = index + 1f
|
||||||
val createdAt = data["created_at"]?.jsonPrimitive?.content
|
}
|
||||||
|
}.reversed().apply {
|
||||||
url = "/pools/$id"
|
if (isNotEmpty()) {
|
||||||
|
this[0].date_upload = dateFormat.tryParse(data.updatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = "/pools/${data.id}"
|
||||||
name = "Oneshot"
|
name = "Oneshot"
|
||||||
date_upload = createdAt?.let(::parseTimestamp) ?: 0
|
date_upload = dateFormat.tryParse(data.updatedAt)
|
||||||
chapter_number = 0F
|
chapter_number = 0F
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
override fun chapterListSelector(): String =
|
}
|
||||||
throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter =
|
|
||||||
throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request =
|
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> =
|
override fun pageListParse(response: Response): List<Page> =
|
||||||
json.decodeFromString<JsonObject>(response.body.string())
|
if (response.request.url.toString().contains("/posts/")) {
|
||||||
.get("post_ids")?.jsonArray
|
val data = response.parseAs<Post>()
|
||||||
?.map { it.jsonPrimitive.content }
|
|
||||||
?.mapIndexed { i, id -> Page(index = i, url = "/posts/$id") }
|
|
||||||
?: emptyList()
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> =
|
listOf(
|
||||||
throw IllegalStateException("Not used")
|
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 =
|
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 =
|
override fun imageUrlParse(response: Response): String =
|
||||||
json.decodeFromString<JsonObject>(response.body.string())
|
response.parseAs<Post>().fileUrl
|
||||||
.get("file_url")!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String =
|
|
||||||
throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String =
|
override fun getChapterUrl(chapter: SChapter): String =
|
||||||
baseUrl + chapter.url
|
baseUrl + chapter.url
|
||||||
@ -181,6 +190,20 @@ class Danbooru : ParsedHttpSource() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun parseTimestamp(string: String): Long? =
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
runCatching { dateFormat.parse(string)?.time!! }.getOrNull()
|
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
|
package eu.kanade.tachiyomi.extension.all.danbooru
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
internal class FilterTags : Filter.Text("Tags")
|
internal class FilterTags : Filter.Text("Tags")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'DeviantArt'
|
extName = 'DeviantArt'
|
||||||
extClass = '.DeviantArt'
|
extClass = '.DeviantArt'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.deviantart
|
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -20,8 +20,6 @@ import okhttp3.Response
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -32,9 +30,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
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")
|
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 {
|
ext {
|
||||||
extName = 'E-Hentai'
|
extName = 'E-Hentai'
|
||||||
extClass = '.EHFactory'
|
extClass = '.EHFactory'
|
||||||
extVersionCode = 24
|
extVersionCode = 25
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.CookieManager
|
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.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@ -30,8 +30,6 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
abstract class EHentai(
|
abstract class EHentai(
|
||||||
@ -41,9 +39,7 @@ abstract class EHentai(
|
|||||||
|
|
||||||
override val name = "E-Hentai"
|
override val name = "E-Hentai"
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||||
private val memberId: String by lazy { getMemberIdPref() }
|
private val memberId: String by lazy { getMemberIdPref() }
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Galaxy'
|
extName = 'Galaxy'
|
||||||
extClass = '.GalaxyFactory'
|
extClass = '.GalaxyFactory'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import uy.kohesive.injekt.Injekt
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class GalaxyFactory : SourceFactory {
|
class GalaxyFactory : SourceFactory {
|
||||||
|
|
||||||
@ -22,9 +20,7 @@ class GalaxyFactory : SourceFactory {
|
|||||||
|
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
||||||
|
@ -4,6 +4,7 @@ ext {
|
|||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://grabber.zone'
|
baseUrl = 'https://grabber.zone'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 36
|
extVersionCode = 38
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.hitomi
|
package eu.kanade.tachiyomi.extension.all.hitomi
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -15,34 +10,28 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import okhttp3.internal.http2.StreamResetException
|
import okhttp3.internal.http2.StreamResetException
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@ -50,35 +39,25 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
class Hitomi(
|
class Hitomi(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val nozomiLang: String,
|
private val nozomiLang: String,
|
||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource() {
|
||||||
|
|
||||||
override val name = "Hitomi"
|
override val name = "Hitomi"
|
||||||
|
|
||||||
private val domain = "hitomi.la"
|
private val cdnDomain = "gold-usergeneratedcontent.net"
|
||||||
|
|
||||||
override val baseUrl = "https://$domain"
|
override val baseUrl = "https://hitomi.la"
|
||||||
|
|
||||||
private val ltnUrl = "https://ltn.$domain"
|
private val ltnUrl = "https://ltn.$cdnDomain"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::jxlContentTypeInterceptor)
|
|
||||||
.addInterceptor(::updateImageUrlInterceptor)
|
.addInterceptor(::updateImageUrlInterceptor)
|
||||||
.apply {
|
.apply {
|
||||||
interceptors().add(0, ::streamResetRetry)
|
interceptors().add(0, ::streamResetRetry)
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!!
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("referer", "$baseUrl/")
|
.set("referer", "$baseUrl/")
|
||||||
.set("origin", baseUrl)
|
.set("origin", baseUrl)
|
||||||
@ -523,7 +502,7 @@ class Hitomi(
|
|||||||
val imageId = imageIdFromHash(hash)
|
val imageId = imageIdFromHash(hash)
|
||||||
val subDomain = 'a' + subdomainOffset(imageId)
|
val subDomain = 'a' + subdomainOffset(imageId)
|
||||||
|
|
||||||
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
|
"https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
|
||||||
}
|
}
|
||||||
description = buildString {
|
description = buildString {
|
||||||
japaneseTitle?.let {
|
japaneseTitle?.let {
|
||||||
@ -568,11 +547,7 @@ class Hitomi(
|
|||||||
name = "Chapter"
|
name = "Chapter"
|
||||||
url = gallery.galleryurl
|
url = gallery.galleryurl
|
||||||
scanlator = gallery.type
|
scanlator = gallery.type
|
||||||
date_upload = try {
|
date_upload = dateFormat.tryParse(gallery.date.substringBeforeLast("-"))
|
||||||
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -589,28 +564,18 @@ class Hitomi(
|
|||||||
return GET("$ltnUrl/galleries/$id.js", headers)
|
return GET("$ltnUrl/galleries/$id.js", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response) = runBlocking {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val gallery = response.parseScriptAs<Gallery>()
|
val gallery = response.parseScriptAs<Gallery>()
|
||||||
val id = gallery.galleryurl
|
val id = gallery.galleryurl
|
||||||
.substringAfterLast("-")
|
.substringAfterLast("-")
|
||||||
.substringBefore(".")
|
.substringBefore(".")
|
||||||
|
|
||||||
gallery.files.mapIndexed { idx, img ->
|
return gallery.files.mapIndexed { idx, img ->
|
||||||
val hash = img.hash
|
// actual logic in updateImageUrlInterceptor
|
||||||
|
val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
|
||||||
val typePref = imageType()
|
.fragment(img.hash)
|
||||||
val avif = img.hasavif == 1 && typePref == "avif"
|
.build()
|
||||||
val jxl = img.hasjxl == 1 && typePref == "jxl"
|
.toString()
|
||||||
|
|
||||||
val commonId = commonImageId()
|
|
||||||
val imageId = imageIdFromHash(hash)
|
|
||||||
val subDomain = 'a' + subdomainOffset(imageId)
|
|
||||||
|
|
||||||
val imageUrl = when {
|
|
||||||
jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl"
|
|
||||||
avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif"
|
|
||||||
else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp"
|
|
||||||
}
|
|
||||||
|
|
||||||
Page(
|
Page(
|
||||||
idx,
|
idx,
|
||||||
@ -636,7 +601,7 @@ class Hitomi(
|
|||||||
val body = use { it.body.string() }
|
val body = use { it.body.string() }
|
||||||
val transformed = transform(body)
|
val transformed = transform(body)
|
||||||
|
|
||||||
return json.decodeFromString(transformed)
|
return transformed.parseAs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun Call.awaitSuccess() =
|
private suspend fun Call.awaitSuccess() =
|
||||||
@ -698,45 +663,6 @@ class Hitomi(
|
|||||||
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
|
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_IMAGETYPE
|
|
||||||
title = "Images Type"
|
|
||||||
entries = arrayOf("webp", "avif", "jxl")
|
|
||||||
entryValues = arrayOf("webp", "avif", "jxl")
|
|
||||||
summary = "Clear chapter cache to apply changes"
|
|
||||||
setDefaultValue("webp")
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<Int>.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray()
|
|
||||||
private val signatureOne = listOf(0xFF, 0x0A).toBytesList()
|
|
||||||
private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList()
|
|
||||||
fun ByteArray.startsWith(byteArray: ByteArray): Boolean {
|
|
||||||
if (this.size < byteArray.size) return false
|
|
||||||
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val response = chain.proceed(chain.request())
|
|
||||||
if (response.headers["Content-Type"] != "application/octet-stream") {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong()
|
|
||||||
val bytesArray = response.peekBody(bytesPeek).bytes()
|
|
||||||
if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
val type = "image/jxl"
|
|
||||||
val body = response.body.bytes().toResponseBody(type.toMediaType())
|
|
||||||
return response.newBuilder()
|
|
||||||
.body(body)
|
|
||||||
.header("Content-Type", type)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun streamResetRetry(chain: Interceptor.Chain): Response {
|
private fun streamResetRetry(chain: Interceptor.Chain): Response {
|
||||||
return try {
|
return try {
|
||||||
chain.proceed(chain.request())
|
chain.proceed(chain.request())
|
||||||
@ -753,21 +679,22 @@ class Hitomi(
|
|||||||
|
|
||||||
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
|
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
|
if (request.url.host != "127.0.0.1") {
|
||||||
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
|
return chain.proceed(request)
|
||||||
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
|
|
||||||
val (ext, hash) = match.destructured
|
|
||||||
|
|
||||||
val commonId = runBlocking { commonImageId() }
|
|
||||||
val imageId = imageIdFromHash(hash)
|
|
||||||
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
|
|
||||||
|
|
||||||
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
|
|
||||||
val newRequest = request.newBuilder().url(newUrl).build()
|
|
||||||
return chain.proceed(newRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chain.proceed(request)
|
val hash = request.url.fragment!!
|
||||||
|
val commonId = runBlocking { commonImageId() }
|
||||||
|
val imageId = imageIdFromHash(hash)
|
||||||
|
val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
|
||||||
|
|
||||||
|
val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
|
||||||
|
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(imageUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(newRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
@ -777,8 +704,4 @@ class Hitomi(
|
|||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREF_IMAGETYPE = "pref_image_type"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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