Compare commits
192 Commits
e0bb39f99a
...
cb27948307
Author | SHA1 | Date |
---|---|---|
Chopper | cb27948307 | |
Chaos Pjeles | dc64c90e7e | |
Vetle Ledaal | 17fc1acd6c | |
Chopper | da0bb1b8eb | |
Chopper | eb4a349e5e | |
Chopper | 7ce559d436 | |
Chopper | 647126a841 | |
Chopper | 26dfd922c4 | |
BrutuZ | aafa0be66d | |
AwkwardPeak7 | 5b7cfb5ca8 | |
KirinRaikage | 996b1138d1 | |
AwkwardPeak7 | a8a540de77 | |
Chopper | 5cc6963a46 | |
bapeey | c5395cae72 | |
bapeey | 49ab904eaa | |
bapeey | d7418f4b5f | |
Chopper | b1c2054428 | |
Chopper | 00c5695acf | |
Chopper | 4b35ae0c78 | |
Chopper | a6da43e9aa | |
kana-shii | 401232bc33 | |
kana-shii | dd897e1499 | |
Chopper | 18943c8c1f | |
bapeey | f740c319ba | |
bapeey | a05b16088f | |
Chopper | 0baa45742a | |
Chopper | 3804bafd6c | |
Chopper | 00a70bc406 | |
Luqman | dc66595ecd | |
Luqman | 1376e3e5dd | |
bapeey | 01b431754f | |
bapeey | ec2dcf0e7b | |
vifixurl | 2f02d090b8 | |
bapeey | 38567075ae | |
Chopper | 033f5cb3e6 | |
Chopper | d2d69b3dcf | |
Chopper | 1290a32547 | |
Chopper | 421c4076d5 | |
bapeey | b404fe1ab0 | |
Chopper | 5ada452655 | |
TheKingTermux | 63f23e44a2 | |
Chopper | 779560170b | |
Chopper | e6a3201790 | |
Chopper | ce931d0f1b | |
Eddiesti | 6605a0af10 | |
Eshlender | 3d060492e6 | |
bapeey | 28d71dd743 | |
Chopper | 8914fc42dd | |
Chopper | 61f7d83728 | |
bapeey | 1767c5a29d | |
bapeey | e91a347360 | |
Vetle Ledaal | 3eb426c197 | |
Lefan | 3477924565 | |
Romain | 07d68def99 | |
Romain | 7dd51c9b1d | |
oalieno | e1538a3be9 | |
Chopper | 1cfbe474eb | |
Lefan | a248121b04 | |
Chopper | 1015e91677 | |
oalieno | 24fc1ba08a | |
oalieno | dcd0ea5b37 | |
zhongfly | d1b75272d0 | |
Lefan | 86c7d2e64b | |
anenasa | bcb6f6972b | |
AwkwardPeak7 | 72d2c38a6d | |
KirinRaikage | f59cd7fd0d | |
Chopper | 2451072c34 | |
ringosham | 23e2192290 | |
Chopper | a1efe96b9d | |
Chopper | 15006c7e96 | |
Chopper | 4fa56b785d | |
Chopper | 76741e7ce5 | |
Chopper | ca55ea1d3b | |
Chopper | 651ed9f3cf | |
AlphaBoom | 2d7101e19b | |
KenjieDec | eb397b2b5f | |
Paco Chrispijn | 4a105eb6ed | |
bapeey | bb5c5a92fe | |
KenjieDec | c56fd24df9 | |
Chopper | 458fde8e7f | |
bapeey | 1e21288bed | |
bapeey | 351e1ce48e | |
bapeey | 6deb8233fc | |
bapeey | bb9103e44a | |
Chopper | 91543c7e10 | |
Chopper | 6231261e72 | |
Chopper | 91e8561f76 | |
Eddiesti | e140f6e543 | |
vifixurl | b00aedee39 | |
Luqman | 8003ab180d | |
Vetle Ledaal | b1bda6d46a | |
Vetle Ledaal | bfbd83b47c | |
Vetle Ledaal | 2a247808f1 | |
Chopper | c13d6b16e2 | |
Chopper | 0b8fdbad93 | |
SummonHIM | db21462070 | |
Chopper | ced338c8e2 | |
Chopper | c24b4d6b98 | |
Chopper | c423d85e2e | |
bapeey | f584c2d169 | |
Chopper | 4a2951e1f0 | |
Vetle Ledaal | 74ad28a16e | |
Vetle Ledaal | fcb27dae61 | |
KenjieDec | e919ddfe06 | |
Chopper | a145f79f35 | |
Smol Ame | 4125032228 | |
Luqman | 843fa52764 | |
AwkwardPeak7 | ac7b2a2dbb | |
Chopper | 6c88ef39cc | |
Chopper | 25cca5e9db | |
Chopper | d74fec37c2 | |
Chopper | 9fe318655e | |
anenasa | 2e672c3c45 | |
lamaxama | be7c3d774a | |
Chopper | 3c4efce496 | |
bapeey | 1e2b6e01df | |
Vetle Ledaal | 4a90f72f0a | |
Vetle Ledaal | fd131cfffb | |
Vetle Ledaal | 243a7e9cd7 | |
Vetle Ledaal | 9dca49b7d9 | |
Vetle Ledaal | 3d7434f88f | |
AwkwardPeak7 | cd5b952013 | |
lamaxama | e0f76c24bd | |
Chopper | f1cb28d2ad | |
kana-shii | 5507a0e6b2 | |
Chopper | 38c3ab9647 | |
Chopper | bacae6ab8e | |
KenjieDec | b86a8c4137 | |
bapeey | 79690a5934 | |
bapeey | 1f160d3352 | |
bapeey | f9c1ba9daf | |
bapeey | eef6f38cd7 | |
bapeey | 12528d72a5 | |
Vetle Ledaal | a38e1bcab1 | |
Vetle Ledaal | cefd29ff16 | |
Vetle Ledaal | 87207fcbfd | |
Milo Mighdoll | 39cbf99fa5 | |
Eddiesti | 3e2037df5a | |
Vetle Ledaal | 0b9daed773 | |
Vetle Ledaal | 51c1961ec5 | |
Vetle Ledaal | 8c78d137ca | |
bapeey | 5a827719cf | |
ringosham | cddbfcfc1a | |
anenasa | dd882e091b | |
Vetle Ledaal | 0353829445 | |
Secozzi | ad7b208e4b | |
Vetle Ledaal | 4938b5b609 | |
Vetle Ledaal | 7477160454 | |
kana-shii | 683f874db5 | |
kana-shii | 631709f29a | |
vifixurl | 1e7fe5c466 | |
Vetle Ledaal | dce175cef5 | |
Vetle Ledaal | 1a6c21f9f3 | |
Vetle Ledaal | e34377d2ad | |
Vetle Ledaal | 2e1a7dc649 | |
Eddiesti | dd9b394388 | |
Vetle Ledaal | 41039a1bc0 | |
Vetle Ledaal | 0d6631ee22 | |
bapeey | e935556bff | |
Cuong-Tran | 394cfb9b13 | |
lamaxama | a2917fa1f0 | |
kana-shii | 38a6e8b12b | |
kana-shii | 660abdf599 | |
KenjieDec | e145bdb6bc | |
Chopper | 1eb5b7e347 | |
zhongfly | 5f905c713d | |
Chopper | 59e9181bf9 | |
Chopper | 74b97ebc23 | |
Chopper | 79b98463cd | |
bapeey | 7f71f15629 | |
Chopper | a0d504b109 | |
Chopper | ceb13d67d9 | |
Chopper | d1d53c61fe | |
Chopper | 6b9659a92c | |
Chopper | de5048fb63 | |
Chopper | c352aedae1 | |
Chopper | 9c71b92ecf | |
Dani | ec8a7f0190 | |
Cuong-Tran | 660f49ee96 | |
KirinRaikage | 931d8fe2f6 | |
anenasa | d1b65b192f | |
bapeey | 5aa0cd2afa | |
Chopper | 66eb921df6 | |
anenasa | a0715ec24a | |
kana-shii | 3ab425a549 | |
kana-shii | 8d408422a4 | |
bapeey | 3a811582b0 | |
bapeey | 89f6711e2e | |
sinkableShip | e45ee68991 | |
Chopper | 0cbd5cc915 | |
Vetle Ledaal | 99605056e3 | |
anenasa | 0e08a7c946 |
|
@ -86,12 +86,9 @@ small, just do a normal full clone instead.**
|
|||
```bash
|
||||
git sparse-checkout set --cone --sparse-index
|
||||
# add project folders
|
||||
git sparse-checkout add .run buildSrc core gradle lib multisrc/src/main/java/generator
|
||||
git sparse-checkout add buildSrc core gradle lib lib-multisrc
|
||||
# add a single source
|
||||
git sparse-checkout add src/<lang>/<source>
|
||||
# add a multisrc theme
|
||||
git sparse-checkout add multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/<source>
|
||||
git sparse-checkout add multisrc/overrides/<source>
|
||||
```
|
||||
|
||||
To remove a source, open `.git/info/sparse-checkout` and delete the exact
|
||||
|
@ -112,13 +109,11 @@ small, just do a normal full clone instead.**
|
|||
```bash
|
||||
/*
|
||||
!/src/*
|
||||
!/multisrc/overrides/*
|
||||
!/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/*
|
||||
!/multisrc-lib/*
|
||||
# allow a single source
|
||||
/src/<lang>/<source>
|
||||
# allow a multisrc theme
|
||||
/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/<source>
|
||||
/multisrc/overrides/<source>
|
||||
/lib-multisrc/<source>
|
||||
# or type the source name directly
|
||||
<source>
|
||||
```
|
||||
|
@ -842,6 +837,15 @@ of `mitmweb`.
|
|||
APKs can be created in Android Studio via `Build > Build Bundle(s) / APK(s) > Build APK(s)` or
|
||||
`Build > Generate Signed Bundle / APK`.
|
||||
|
||||
If for some reason you decide to build the APK from the command line, you can use the following
|
||||
command (because you're doing things differently than expected, I assume you have some
|
||||
knowledge of gradlew and your OS):
|
||||
|
||||
```console
|
||||
// For a single apk, use this command
|
||||
$ ./gradlew src:<lang>:<source>:assembleDebug
|
||||
```
|
||||
|
||||
## Submitting the changes
|
||||
|
||||
When you feel confident about your changes, submit a new Pull Request so your code can be reviewed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -4,7 +4,7 @@ coroutines_version = "1.6.4"
|
|||
serialization_version = "1.4.0"
|
||||
|
||||
[libraries]
|
||||
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.4.1" }
|
||||
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.6.1" }
|
||||
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
||||
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
||||
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
|
@ -55,7 +57,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
@ -84,7 +86,8 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.multisrc.blogtruyen
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
@ -366,7 +367,11 @@ abstract class BlogTruyen(
|
|||
)
|
||||
|
||||
Single.fromCallable {
|
||||
client.newCall(request).execute().close()
|
||||
try {
|
||||
client.newCall(request).execute().close()
|
||||
} catch (e: Exception) {
|
||||
Log.e("BlogTruyen", "Error updating view count", e)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
|
|
@ -39,7 +39,7 @@ open class GoDa(
|
|||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup().also(::parseGenres)
|
||||
val mangas = document.select(".cardlist .pb-2 a").map { element ->
|
||||
val mangas = document.select(".container > .cardlist .pb-2 a").map { element ->
|
||||
SManga.create().apply {
|
||||
val imgSrc = element.selectFirst("img")!!.attr("src")
|
||||
url = getKey(element.attr("href"))
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cookieinterceptor"))
|
||||
|
|
|
@ -120,7 +120,7 @@ abstract class HotComics(
|
|||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().select("#tab-chapter a").map { element ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
setUrlWithoutDomain(element.attr("onclick").substringAfter("popupLogin('").substringBefore("'"))
|
||||
name = element.selectFirst(".cell-num")!!.text()
|
||||
date_upload = parseDate(element.selectFirst(".cell-time")?.text())
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
||||
baseVersionCode = 14
|
||||
|
|
|
@ -4,29 +4,26 @@ import android.app.Application
|
|||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.multisrc.kemono.KemonoCreatorDto.Companion.serviceName
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.blackholeSink
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.lang.Thread.sleep
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.min
|
||||
|
||||
|
@ -51,6 +48,8 @@ open class Kemono(
|
|||
|
||||
private val imgCdnUrl = baseUrl.replace("//", "//img.")
|
||||
|
||||
private var mangasCache: List<KemonoCreatorDto> = emptyList()
|
||||
|
||||
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
|
||||
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
@ -63,82 +62,123 @@ open class Kemono(
|
|||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return Observable.fromCallable {
|
||||
fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited })
|
||||
searchMangas(page, sortBy = "pop" to "desc")
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return Observable.fromCallable {
|
||||
fetchNewDesignListing(page, "/artists/updated", compareByDescending { it.updatedDate })
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNewDesignListing(
|
||||
page: Int,
|
||||
path: String,
|
||||
comparator: Comparator<KemonoCreatorDto>,
|
||||
): MangasPage {
|
||||
val baseUrl = baseUrl
|
||||
return if (page == 1) {
|
||||
val document = client.newCall(GET(baseUrl + path, headers)).execute().asJsoup()
|
||||
val cardList = document.selectFirst(Evaluator.Class("card-list__items"))!!
|
||||
val creators = cardList.children().map {
|
||||
SManga.create().apply {
|
||||
url = it.attr("href")
|
||||
title = it.selectFirst(Evaluator.Class("user-card__name"))!!.ownText()
|
||||
author = it.selectFirst(Evaluator.Class("user-card__service"))!!.ownText()
|
||||
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.absUrl("src").formatAvatarUrl()
|
||||
description = PROMPT
|
||||
initialized = true
|
||||
}
|
||||
}.filterUnsupported()
|
||||
MangasPage(creators, true).also { cacheCreators() }
|
||||
} else {
|
||||
fetchCreatorsPage(page) { it.apply { sortWith(comparator) } }
|
||||
searchMangas(page, sortBy = "lat" to "desc")
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
||||
if (query.isBlank()) throw Exception("Query is empty")
|
||||
fetchCreatorsPage(page) { all ->
|
||||
val result = all.filterTo(ArrayList()) { it.name.contains(query, ignoreCase = true) }
|
||||
if (result.isEmpty()) return@fetchCreatorsPage emptyList()
|
||||
if (result[0].favorited != -1) {
|
||||
result.sortByDescending { it.favorited }
|
||||
} else {
|
||||
result.sortByDescending { it.updatedDate }
|
||||
searchMangas(page, query, filters)
|
||||
}
|
||||
|
||||
private fun searchMangas(page: Int = 1, title: String = "", filters: FilterList? = null, sortBy: Pair<String, String> = "" to ""): MangasPage {
|
||||
var sort = sortBy
|
||||
val typeIncluded: MutableList<String> = mutableListOf()
|
||||
val typeExcluded: MutableList<String> = mutableListOf()
|
||||
var fav: Boolean? = null
|
||||
filters?.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> {
|
||||
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc"
|
||||
}
|
||||
is TypeFilter -> {
|
||||
filter.state.filter { state -> state.isIncluded() }.forEach { tri ->
|
||||
typeIncluded.add(tri.value)
|
||||
}
|
||||
|
||||
filter.state.filter { state -> state.isExcluded() }.forEach { tri ->
|
||||
typeExcluded.add(tri.value)
|
||||
}
|
||||
}
|
||||
is FavouritesFilter -> {
|
||||
fav = when (filter.state[0].state) {
|
||||
0 -> null
|
||||
1 -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchCreatorsPage(
|
||||
page: Int,
|
||||
block: (ArrayList<KemonoCreatorDto>) -> List<KemonoCreatorDto>,
|
||||
): MangasPage {
|
||||
val imgCdnUrl = this.imgCdnUrl
|
||||
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
|
||||
val allCreators = block(response.parseAs())
|
||||
val count = allCreators.size
|
||||
val fromIndex = (page - 1) * NEW_PAGE_SIZE
|
||||
val toIndex = min(count, fromIndex + NEW_PAGE_SIZE)
|
||||
val creators = allCreators.subList(fromIndex, toIndex)
|
||||
.map { it.toSManga(imgCdnUrl) }
|
||||
.filterUnsupported()
|
||||
return MangasPage(creators, toIndex < count)
|
||||
}
|
||||
var mangas = mangasCache
|
||||
if (page == 1) {
|
||||
var favourites: List<KemonoFavouritesDto> = emptyList()
|
||||
if (fav != null) {
|
||||
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
||||
|
||||
private fun cacheCreators() {
|
||||
val callback = object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) =
|
||||
response.body.source().run {
|
||||
readAll(blackholeSink())
|
||||
close()
|
||||
if (favores.code == 401) throw Exception("You are not Logged In")
|
||||
favourites = favores.parseAs<List<KemonoFavouritesDto>>().filterNot { it.service.lowercase() == "discord" }
|
||||
}
|
||||
|
||||
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
|
||||
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
|
||||
mangas = allCreators.filter {
|
||||
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
|
||||
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
|
||||
|
||||
val regularSearch = it.name.contains(title, true)
|
||||
|
||||
val isFavourited = when (fav) {
|
||||
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
|
||||
false -> favourites.none { f -> f.id == it.id }
|
||||
else -> true
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) = Unit
|
||||
includeType && !excludeType && isFavourited &&
|
||||
regularSearch
|
||||
}.also { mangasCache = mangas }
|
||||
}
|
||||
client.newCall(GET("$baseUrl/$apiPath/creators", headers)).enqueue(callback)
|
||||
|
||||
val sorted = when (sort.first) {
|
||||
"pop" -> {
|
||||
if (sort.second == "desc") {
|
||||
mangas.sortedByDescending { it.favorited }
|
||||
} else {
|
||||
mangas.sortedBy { it.favorited }
|
||||
}
|
||||
}
|
||||
"tit" -> {
|
||||
if (sort.second == "desc") {
|
||||
mangas.sortedByDescending { it.name }
|
||||
} else {
|
||||
mangas.sortedBy { it.name }
|
||||
}
|
||||
}
|
||||
"new" -> {
|
||||
if (sort.second == "desc") {
|
||||
mangas.sortedByDescending { it.id }
|
||||
} else {
|
||||
mangas.sortedBy { it.id }
|
||||
}
|
||||
}
|
||||
"fav" -> {
|
||||
if (fav != true) throw Exception("Please check 'Favourites Only' Filter")
|
||||
if (sort.second == "desc") {
|
||||
mangas.sortedByDescending { it.fav }
|
||||
} else {
|
||||
mangas.sortedBy { it.fav }
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (sort.second == "desc") {
|
||||
mangas.sortedByDescending { it.updatedDate }
|
||||
} else {
|
||||
mangas.sortedBy { it.updatedDate }
|
||||
}
|
||||
}
|
||||
}
|
||||
val maxIndex = mangas.size
|
||||
val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT
|
||||
val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT)
|
||||
|
||||
val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(imgCdnUrl) }
|
||||
return MangasPage(final, toIndex != maxIndex)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
@ -151,33 +191,38 @@ open class Kemono(
|
|||
|
||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url.replace("$apiPath/", "")}"
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||
KemonoPostDto.dateFormat.timeZone = when (manga.author) {
|
||||
"Pixiv Fanbox", "Fantia" -> TimeZone.getTimeZone("GMT+09:00")
|
||||
else -> TimeZone.getTimeZone("GMT")
|
||||
}
|
||||
val maxPosts = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
|
||||
.toInt().coerceAtMost(POST_PAGES_MAX) * POST_PAGE_SIZE
|
||||
val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
|
||||
.toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT
|
||||
var offset = 0
|
||||
var hasNextPage = true
|
||||
val result = ArrayList<SChapter>()
|
||||
while (offset < maxPosts && hasNextPage) {
|
||||
val request = GET("$baseUrl/$apiPath${manga.url}?limit=$POST_PAGE_SIZE&o=$offset", headers)
|
||||
while (offset < prefMaxPost && hasNextPage) {
|
||||
val request = GET("$baseUrl/$apiPath${manga.url}?o=$offset", headers)
|
||||
val page: List<KemonoPostDto> = retry(request).parseAs()
|
||||
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
|
||||
offset += POST_PAGE_SIZE
|
||||
hasNextPage = page.size == POST_PAGE_SIZE
|
||||
offset += PAGE_POST_LIMIT
|
||||
hasNextPage = page.size == PAGE_POST_LIMIT
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private fun retry(request: Request): Response {
|
||||
var code = 0
|
||||
repeat(3) {
|
||||
repeat(5) {
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) return response
|
||||
response.close()
|
||||
code = response.code
|
||||
if (code == 429) {
|
||||
sleep(10000)
|
||||
}
|
||||
}
|
||||
throw Exception("HTTP error $code")
|
||||
}
|
||||
|
@ -217,10 +262,8 @@ open class Kemono(
|
|||
key = POST_PAGES_PREF
|
||||
title = "Maximum posts to load"
|
||||
summary = "Loading more posts costs more time and network traffic.\nCurrently: %s"
|
||||
entryValues = (1..POST_PAGES_MAX).map { it.toString() }.toTypedArray()
|
||||
entries = (1..POST_PAGES_MAX).map {
|
||||
if (it == 1) "1 page ($POST_PAGE_SIZE posts)" else "$it pages (${it * POST_PAGE_SIZE} posts)"
|
||||
}.toTypedArray()
|
||||
entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() }
|
||||
entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" }
|
||||
setDefaultValue(POST_PAGES_DEFAULT)
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
|
@ -232,16 +275,55 @@ open class Kemono(
|
|||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList =
|
||||
FilterList(
|
||||
SortFilter(
|
||||
"Sort by",
|
||||
Filter.Sort.Selection(0, false),
|
||||
getSortsList,
|
||||
),
|
||||
TypeFilter("Types", getTypes),
|
||||
FavouritesFilter(),
|
||||
)
|
||||
|
||||
open val getTypes: List<String> = emptyList()
|
||||
|
||||
open val getSortsList: List<Pair<String, String>> = listOf(
|
||||
Pair("Popularity", "pop"),
|
||||
Pair("Date Indexed", "new"),
|
||||
Pair("Date Updated", "lat"),
|
||||
Pair("Alphabetical Order", "tit"),
|
||||
Pair("Service", "serv"),
|
||||
Pair("Date Favourited", "fav"),
|
||||
)
|
||||
|
||||
internal open class TypeFilter(name: String, vals: List<String>) :
|
||||
Filter.Group<TriFilter>(
|
||||
name,
|
||||
vals.map { TriFilter(it, it.lowercase()) },
|
||||
)
|
||||
|
||||
internal class FavouritesFilter() :
|
||||
Filter.Group<TriFilter>(
|
||||
"Favourites",
|
||||
listOf(TriFilter("Favourites Only", "fav")),
|
||||
)
|
||||
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
|
||||
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
|
||||
fun getValue() = vals[state!!.index].second
|
||||
}
|
||||
companion object {
|
||||
private const val NEW_PAGE_SIZE = 50
|
||||
private const val PAGE_POST_LIMIT = 50
|
||||
private const val PAGE_CREATORS_LIMIT = 50
|
||||
const val PROMPT = "You can change how many posts to load in the extension preferences."
|
||||
|
||||
private const val POST_PAGE_SIZE = 50
|
||||
private const val POST_PAGES_PREF = "POST_PAGES"
|
||||
private const val POST_PAGES_DEFAULT = "1"
|
||||
private const val POST_PAGES_MAX = 50
|
||||
|
||||
private fun List<SManga>.filterUnsupported() = filterNot { it.author == "Discord" }
|
||||
private const val POST_PAGES_MAX = 75
|
||||
|
||||
// private const val BASE_URL_PREF = "BASE_URL"
|
||||
private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG"
|
||||
|
|
|
@ -7,15 +7,23 @@ import kotlinx.serialization.json.JsonPrimitive
|
|||
import kotlinx.serialization.json.double
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
@Serializable
|
||||
class KemonoFavouritesDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val service: String,
|
||||
val faved_seq: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class KemonoCreatorDto(
|
||||
private val id: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
private val service: String,
|
||||
val service: String,
|
||||
private val updated: JsonPrimitive,
|
||||
val favorited: Int = -1,
|
||||
) {
|
||||
var fav: Long = 0
|
||||
val updatedDate get() = when {
|
||||
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
||||
else -> (updated.double * 1000).toLong()
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
pref_show_paid_chapter_title=Display paid chapters
|
||||
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
||||
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
|
|
@ -2,4 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 7
|
||||
baseVersionCode = 8
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package eu.kanade.tachiyomi.multisrc.keyoapp
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -17,6 +23,8 @@ import okhttp3.Request
|
|||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
|
@ -27,7 +35,12 @@ abstract class Keyoapp(
|
|||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
@ -39,6 +52,13 @@ abstract class Keyoapp(
|
|||
|
||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
|
||||
protected val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = "en",
|
||||
availableLanguages = setOf("en"),
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
@ -218,7 +238,12 @@ abstract class Keyoapp(
|
|||
|
||||
// Chapter list
|
||||
|
||||
override fun chapterListSelector(): String = "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
|
||||
override fun chapterListSelector(): String {
|
||||
if (!preferences.showPaidChapters) {
|
||||
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[src*=Coin.svg]))"
|
||||
}
|
||||
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||
|
@ -226,6 +251,9 @@ abstract class Keyoapp(
|
|||
element.selectFirst(".text-xs")?.run {
|
||||
date_upload = text().trim().parseDate()
|
||||
}
|
||||
if (element.select("img[src*=Coin.svg]").isNotEmpty()) {
|
||||
name = "🔒 $name"
|
||||
}
|
||||
}
|
||||
|
||||
// Image list
|
||||
|
@ -235,7 +263,7 @@ abstract class Keyoapp(
|
|||
.map { it.attr("uid") }
|
||||
.filter { it.isNotEmpty() }
|
||||
.mapIndexed { index, img ->
|
||||
Page(index, document.location(), "$cdnUrl/uploads/$img")
|
||||
Page(index, document.location(), "$cdnUrl/$img")
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.also { return it }
|
||||
|
@ -249,7 +277,7 @@ abstract class Keyoapp(
|
|||
}
|
||||
}
|
||||
|
||||
protected val cdnUrl = "https://cdn.igniscans.com"
|
||||
protected open val cdnUrl = "https://2xffbs-cn8.is1.buzz/uploads"
|
||||
|
||||
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
||||
|
||||
|
@ -315,4 +343,22 @@ abstract class Keyoapp(
|
|||
}
|
||||
return now.timeInMillis
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = SHOW_PAID_CHAPTERS_PREF
|
||||
title = intl["pref_show_paid_chapter_title"]
|
||||
summaryOn = intl["pref_show_paid_chapter_summary_on"]
|
||||
summaryOff = intl["pref_show_paid_chapter_summary_off"]
|
||||
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
protected val SharedPreferences.showPaidChapters: Boolean
|
||||
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
|
||||
|
||||
companion object {
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
|
@ -0,0 +1,143 @@
|
|||
package eu.kanade.tachiyomi.multisrc.lectormoe
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class LectorMoe(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val organizationDomain: String = baseUrl.substringAfter("://"),
|
||||
private val apiBaseUrl: String = "https://api.lector.moe",
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 3)
|
||||
.rateLimitHost(apiBaseUrl.toHttpUrl(), 3)
|
||||
.build()
|
||||
|
||||
final override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val apiHeaders: Headers = headersBuilder()
|
||||
.add("Organization-Domain", organizationDomain)
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$apiBaseUrl/api/manga-custom?page=$page&limit=$PAGE_LIMIT&order=popular", apiHeaders)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$apiBaseUrl/api/manga-custom?page=$page&limit=$PAGE_LIMIT&order=latest", apiHeaders)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$apiBaseUrl/api/manga-custom".toHttpUrl().newBuilder()
|
||||
|
||||
url.setQueryParameter("page", page.toString())
|
||||
url.setQueryParameter("limit", PAGE_LIMIT.toString())
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortByFilter -> url.setQueryParameter("order", filter.toUriPart())
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (query.isNotBlank()) url.setQueryParameter("title", query)
|
||||
|
||||
return GET(url.build(), apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val page = response.request.url.queryParameter("page")!!.toInt()
|
||||
val result = json.decodeFromString<Data<SeriesListDataDto>>(response.body.string())
|
||||
|
||||
val mangas = result.data.series.map { it.toSManga() }
|
||||
val hasNextPage = page < result.data.maxPage
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortByFilter("Ordenar por", getSortList()),
|
||||
)
|
||||
|
||||
private fun getSortList() = arrayOf(
|
||||
Pair("Popularidad", "popular"),
|
||||
Pair("Recientes", "latest"),
|
||||
)
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.url}"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request =
|
||||
GET("$apiBaseUrl/api/manga-custom/${manga.url}", apiHeaders)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
|
||||
return result.data.toSMangaDetails()
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
val seriesSlug = chapter.url.substringBefore("/")
|
||||
val chapterSlug = chapter.url.substringAfter("/")
|
||||
|
||||
return "$baseUrl/manga/$seriesSlug/chapters/$chapterSlug"
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
|
||||
val seriesSlug = result.data.slug
|
||||
return result.data.chapters?.map { it.toSChapter(seriesSlug) } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val seriesSlug = chapter.url.substringBefore("/")
|
||||
val chapterSlug = chapter.url.substringAfter("/")
|
||||
|
||||
return GET("$apiBaseUrl/api/manga-custom/$seriesSlug/chapter/$chapterSlug/pages", apiHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = json.decodeFromString<Data<List<PageDto>>>(response.body.string())
|
||||
return result.data.mapIndexed { i, page ->
|
||||
Page(i, imageUrl = page.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
class SortByFilter(title: String, list: Array<Pair<String, String>>) : UriPartFilter(title, list)
|
||||
|
||||
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_LIMIT = 36
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.es.senshimanga
|
||||
package eu.kanade.tachiyomi.multisrc.lectormoe
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 31
|
||||
baseVersionCode = 32
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.app.Application
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
|
@ -102,18 +103,18 @@ abstract class LibGroup(
|
|||
}
|
||||
|
||||
private var _constants: Constants? = null
|
||||
private fun getConstants(): Constants {
|
||||
private fun getConstants(): Constants? {
|
||||
if (_constants == null) {
|
||||
try {
|
||||
_constants = client.newCall(
|
||||
GET("$apiDomain/api/constants?fields[]=genres&fields[]=tags&fields[]=types&fields[]=scanlateStatus&fields[]=status&fields[]=format&fields[]=ageRestriction&fields[]=imageServers", headers),
|
||||
).execute().parseAs<Data<Constants>>().data
|
||||
return _constants!!
|
||||
} catch (ex: SerializationException) {
|
||||
throw Exception("Ошибка сериализации. Проверьте сайт.")
|
||||
return _constants
|
||||
} catch (ex: Exception) {
|
||||
Log.d("LibGroup", "Error getting constants: $ex")
|
||||
}
|
||||
}
|
||||
return _constants!!
|
||||
return _constants
|
||||
}
|
||||
|
||||
private fun checkForToken(chain: Interceptor.Chain): Response {
|
||||
|
@ -376,7 +377,7 @@ abstract class LibGroup(
|
|||
if (page.imageUrl != null) {
|
||||
return Observable.just(page.imageUrl)
|
||||
}
|
||||
val server = getConstants().getServer(isServer(), siteId).url
|
||||
val server = getConstants()?.getServer(isServer(), siteId)?.url ?: throw Exception("Ошибка получения сервера изображений")
|
||||
return Observable.just("$server${page.url}")
|
||||
}
|
||||
|
||||
|
@ -508,13 +509,13 @@ abstract class LibGroup(
|
|||
|
||||
filters += if (_constants != null) {
|
||||
listOf(
|
||||
CategoryList(getConstants().getCategories(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
FormatList(getConstants().getFormats(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
GenreList(getConstants().getGenres(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
TagList(getConstants().getTags(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
StatusList(getConstants().getScanlateStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
StatusTitleList(getConstants().getTitleStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
AgeList(getConstants().getAgeRestrictions(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
CategoryList(getConstants()!!.getCategories(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
FormatList(getConstants()!!.getFormats(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
GenreList(getConstants()!!.getGenres(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
TagList(getConstants()!!.getTags(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||
StatusList(getConstants()!!.getScanlateStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
StatusTitleList(getConstants()!!.getTitleStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
AgeList(getConstants()!!.getAgeRestrictions(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
|
|
|
@ -42,6 +42,10 @@ abstract class MangaEsp(
|
|||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
protected open val apiPath = "/api"
|
||||
|
||||
protected open val seriesPath = "/ver"
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
|
@ -49,7 +53,7 @@ abstract class MangaEsp(
|
|||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/api/topSerie", headers)
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl$apiPath/topSerie", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<TopSeriesDto>(response.body.string())
|
||||
|
@ -58,17 +62,17 @@ abstract class MangaEsp(
|
|||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga() }
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl/api/lastUpdates", headers)
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl$apiPath/lastUpdates", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
||||
|
||||
val mangas = responseData.response.map { it.toSManga() }
|
||||
val mangas = responseData.response.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
@ -151,7 +155,7 @@ abstract class MangaEsp(
|
|||
|
||||
return MangasPage(
|
||||
filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size))
|
||||
.map { it.toSManga() },
|
||||
.map { it.toSManga(seriesPath) },
|
||||
hasNextPage,
|
||||
)
|
||||
}
|
||||
|
@ -171,7 +175,7 @@ abstract class MangaEsp(
|
|||
?: throw Exception(intl["comic_data_error"])
|
||||
val unescapedJson = mangaDetailsJson.unescape()
|
||||
val series = json.decodeFromString<SeriesDto>(unescapedJson)
|
||||
return series.chapters.map { it.toSChapter(series.slug) }
|
||||
return series.chapters.map { it.toSChapter(seriesPath, series.slug) }
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
|
|
|
@ -48,11 +48,11 @@ class SeriesDto(
|
|||
@SerialName("idioma")
|
||||
val language: String? = null,
|
||||
) {
|
||||
fun toSManga(): SManga {
|
||||
fun toSManga(seriesPath: String): SManga {
|
||||
return SManga.create().apply {
|
||||
title = name
|
||||
thumbnail_url = thumbnail
|
||||
url = "/ver/$slug"
|
||||
url = "$seriesPath/$slug"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ class ChapterDto(
|
|||
private val slug: String,
|
||||
@SerialName("created_at") private val date: String,
|
||||
) {
|
||||
fun toSChapter(seriesSlug: String): SChapter {
|
||||
fun toSChapter(seriesPath: String, seriesSlug: String): SChapter {
|
||||
return SChapter.create().apply {
|
||||
name = "Capítulo ${number.toString().removeSuffix(".0")}"
|
||||
if (!this@ChapterDto.name.isNullOrBlank()) {
|
||||
|
@ -115,7 +115,7 @@ class ChapterDto(
|
|||
} catch (e: Exception) {
|
||||
0L
|
||||
}
|
||||
url = "/ver/$seriesSlug/$slug"
|
||||
url = "$seriesPath/$seriesSlug/$slug"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ open class MCCMSConfig(
|
|||
hasCategoryPage: Boolean = true,
|
||||
val textSearchOnlyPageOne: Boolean = false,
|
||||
val useMobilePageList: Boolean = false,
|
||||
protected val lazyLoadImageAttr: String = "data-original",
|
||||
private val lazyLoadImageAttr: String = "data-original",
|
||||
) {
|
||||
val genreData = GenreData(hasCategoryPage)
|
||||
|
||||
open fun pageListParse(response: Response): List<Page> {
|
||||
fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return if (useMobilePageList) {
|
||||
|
|
|
@ -131,8 +131,8 @@ abstract class WPComics(
|
|||
}
|
||||
|
||||
open fun String?.toStatus(): Int {
|
||||
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "連載中")
|
||||
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "完結済み")
|
||||
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "Đang cập nhật", "連載中")
|
||||
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "Đã hoàn thành", "完結済み")
|
||||
return when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
||||
|
|
|
@ -114,7 +114,7 @@ abstract class ZeistManga(
|
|||
val result = json.decodeFromString<ZeistMangaDto>(jsonString)
|
||||
|
||||
val mangas = result.feed?.entry.orEmpty()
|
||||
.filter { it.category.orEmpty().any { category -> category.term == "Series" } } // Default category for all series
|
||||
.filter { it.category.orEmpty().any { category -> category.term == mangaCategory } }
|
||||
.filterNot { it.category.orEmpty().any { category -> excludedCategories.contains(category.term) } }
|
||||
.map { it.toSManga(baseUrl) }
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 37
|
||||
extVersionCode = 41
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ open class BatoTo(
|
|||
}
|
||||
|
||||
override val name: String = "Bato.to"
|
||||
override val baseUrl: String = getMirrorPref()!!
|
||||
override val baseUrl: String by lazy { getMirrorPref()!! }
|
||||
override val id: Long = when (lang) {
|
||||
"zh-Hans" -> 2818874445640189582
|
||||
"zh-Hant" -> 38886079663327225
|
||||
|
@ -88,12 +88,25 @@ open class BatoTo(
|
|||
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
val removeOfficialPref = CheckBoxPreference(screen.context).apply {
|
||||
key = "${REMOVE_TITLE_VERSION_PREF}_$lang"
|
||||
title = "Remove version information from entry titles"
|
||||
summary = "This removes version tags like '(Official)' or '(Yaoi)' from entry titles " +
|
||||
"and helps identify duplicate entries in your library. " +
|
||||
"To update existing entries, remove them from your library (unfavorite) and refresh manually. " +
|
||||
"You might also want to clear the database in advanced settings."
|
||||
setDefaultValue(false)
|
||||
}
|
||||
screen.addPreference(mirrorPref)
|
||||
screen.addPreference(altChapterListPref)
|
||||
screen.addPreference(removeOfficialPref)
|
||||
}
|
||||
|
||||
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||
private fun isRemoveTitleVersion(): Boolean {
|
||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
private val json: Json by injectLazy()
|
||||
|
@ -309,23 +322,34 @@ open class BatoTo(
|
|||
}
|
||||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
private var titleRegex: Regex =
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|/.+?)\\s*|([|/~].*)|-.*-")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
val manga = SManga.create()
|
||||
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||
manga.title = infoElement.select("h3").text().removeEntities()
|
||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
||||
val alternativeTitles = document.select("div.pb-2.alias-set.line-b-f").text()
|
||||
val description = infoElement.select("div.limit-html").text() + "\n" +
|
||||
infoElement.select(".episode-list > .alert-warning").text().trim()
|
||||
val cleanedTitle = if (isRemoveTitleVersion()) {
|
||||
originalTitle.replace(titleRegex, "").trim()
|
||||
} else {
|
||||
originalTitle
|
||||
}
|
||||
|
||||
manga.title = cleanedTitle
|
||||
manga.author = infoElement.select("div.attr-item:contains(author) span").text()
|
||||
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
|
||||
manga.status = parseStatus(workStatus, uploadStatus)
|
||||
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
||||
manga.description = infoElement.select("div.limit-html").text() + "\n" + infoElement.select(".episode-list > .alert-warning").text().trim()
|
||||
manga.thumbnail_url = document.select("div.attr-cover img")
|
||||
.attr("abs:src")
|
||||
manga.description = description +
|
||||
if (alternativeTitles.isNotBlank()) "\n\nAlternative Titles:\n$alternativeTitles" else ""
|
||||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
|
||||
workStatus == null -> SManga.UNKNOWN
|
||||
workStatus.contains("Ongoing") -> SManga.ONGOING
|
||||
|
@ -945,6 +969,7 @@ open class BatoTo(
|
|||
companion object {
|
||||
private const val MIRROR_PREF_KEY = "MIRROR"
|
||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||
"bato.to",
|
||||
"batocomic.com",
|
||||
|
@ -962,6 +987,8 @@ open class BatoTo(
|
|||
"readtoto.net",
|
||||
"readtoto.org",
|
||||
"dto.to",
|
||||
"fto.to",
|
||||
"jto.to",
|
||||
"hto.to",
|
||||
"mto.to",
|
||||
"wto.to",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 47
|
||||
extVersionCode = 48
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -329,7 +329,11 @@ abstract class Comick(
|
|||
is TagFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
it.state.split(",").forEach {
|
||||
addQueryParameter("tags", it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-").replace("'-", "-and-039-").replace("'", "-and-039-"))
|
||||
addQueryParameter(
|
||||
"tags",
|
||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -372,29 +376,26 @@ abstract class Comick(
|
|||
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
||||
val mangaData = response.parseAs<Manga>()
|
||||
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
|
||||
if (manga.thumbnail_url.toString().endsWith("#1")) {
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
covers = listOf(
|
||||
MDcovers(
|
||||
b2key = manga.thumbnail_url?.substringBeforeLast("#")
|
||||
?.substringAfterLast("/"),
|
||||
vol = "1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
val coversUrl =
|
||||
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
|
||||
val covers = client.newCall(GET(coversUrl)).execute()
|
||||
.parseAs<Covers>().mdCovers.reversed()
|
||||
.parseAs<Covers>().mdCovers.reversed().toMutableList()
|
||||
if (covers.any { it.vol == "1" }) covers.retainAll { it.vol == "1" }
|
||||
if (
|
||||
covers.any { it.locale == comickLang.split('-').first() }
|
||||
) {
|
||||
covers.retainAll { it.locale == comickLang.split('-').first() }
|
||||
}
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
covers = if (covers.any { it.vol == "1" }) covers.filter { it.vol == "1" } else covers,
|
||||
scorePosition = preferences.scorePosition,
|
||||
covers = covers,
|
||||
)
|
||||
}
|
||||
return mangaData.toSManga(includeMuTags = preferences.includeMuTags)
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
|
@ -511,12 +512,12 @@ abstract class Comick(
|
|||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||
private const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
|
||||
private const val FIRST_COVER_PREF = "DefaultCover"
|
||||
private const val FIRST_COVER_DEFAULT = true
|
||||
private const val SCORE_POSITION_PREF = "ScorePosition"
|
||||
private const val SCORE_POSITION_DEFAULT = "top"
|
||||
const val SCORE_POSITION_DEFAULT = "top"
|
||||
private const val LIMIT = 20
|
||||
private const val CHAPTERS_LIMIT = 99999
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||
|
||||
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.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
|
@ -31,8 +33,8 @@ class Manga(
|
|||
private val demographic: String? = null,
|
||||
) {
|
||||
fun toSManga(
|
||||
includeMuTags: Boolean = false,
|
||||
scorePosition: String = "",
|
||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
||||
covers: List<MDcovers>? = null,
|
||||
) =
|
||||
SManga.create().apply {
|
||||
|
@ -148,6 +150,7 @@ class Covers(
|
|||
class MDcovers(
|
||||
val b2key: String?,
|
||||
val vol: String? = null,
|
||||
val locale: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -2,4 +2,10 @@ package eu.kanade.tachiyomi.extension.all.coomer
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
||||
|
||||
class Coomer : Kemono("Coomer", "https://coomer.su", "all")
|
||||
class Coomer : Kemono("Coomer", "https://coomer.su", "all") {
|
||||
override val getTypes = listOf(
|
||||
"OnlyFans",
|
||||
"Fansly",
|
||||
"CandFans",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'CosplayTele'
|
||||
extClass = '.CosplayTele'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -66,17 +66,17 @@ class CosplayTele : ParsedHttpSource() {
|
|||
}
|
||||
|
||||
private val popularPageLimit = 20
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/wp-json/wordpress-popular-posts/v1/popular-posts?offset=${page * popularPageLimit}&limit=$popularPageLimit&range=last7days")
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/wp-json/wordpress-popular-posts/v1/popular-posts?offset=${page * popularPageLimit}&limit=$popularPageLimit&range=last7days&embed=true&_embed=wp:featuredmedia&_fields=title,link,_embedded,_links.wp:featuredmedia")
|
||||
override fun popularMangaSelector(): String = ""
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val jsonObject = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val mangas = jsonObject.map { item ->
|
||||
val head = item.jsonObject["yoast_head_json"]!!.jsonObject
|
||||
val respObject = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val mangas = respObject.map { item ->
|
||||
SManga.create().apply {
|
||||
title = head["og_title"]!!.jsonPrimitive.content
|
||||
thumbnail_url = head["og_image"]!!.jsonArray[0].jsonObject["url"]!!.jsonPrimitive.content
|
||||
setUrlWithoutDomain(head["og_url"]!!.jsonPrimitive.content)
|
||||
title = item.jsonObject!!["title"]!!.jsonObject!!["rendered"]!!.jsonPrimitive.content
|
||||
thumbnail_url = item.jsonObject!!["_embedded"]!!.jsonObject!!["wp:featuredmedia"]!!.jsonArray[0]!!.jsonObject["source_url"]!!.jsonPrimitive.content
|
||||
setUrlWithoutDomain(item.jsonObject!!["link"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
return MangasPage(mangas, mangas.size >= popularPageLimit)
|
||||
|
|
|
@ -8,7 +8,7 @@ import android.util.Log
|
|||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://e-hentai.net/g/xxxxx/yyyyy/ intents and redirects them to
|
||||
* Springboard that accepts https://e-hentai.org/g/xxxxx/yyyyy/ intents and redirects them to
|
||||
* the main Tachiyomi process.
|
||||
*/
|
||||
class EHUrlActivity : Activity() {
|
||||
|
|
|
@ -32,14 +32,14 @@ open class EternalMangas(
|
|||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.filter { it.language == internalLang }
|
||||
.map { it.toSManga() }
|
||||
.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga() } ?: emptyList()
|
||||
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga(seriesPath) } ?: emptyList()
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,167 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaicafe
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class HentaiCafe : ParsedHttpSource() {
|
||||
|
||||
override val name = "Hentai Cafe"
|
||||
|
||||
override val baseUrl = "https://hentaicafe.xxx"
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client by lazy {
|
||||
network.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
// Image CDN
|
||||
.rateLimitHost("https://cdn.hentaibomb.com".toHttpUrl(), 2)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Accept-Language", "en-US,en;q=0.5")
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.index-popular > div.gallery > a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")?.getImageUrl()
|
||||
title = element.selectFirst("div.caption")!!.text()
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
|
||||
|
||||
override fun latestUpdatesSelector() = "div.index-container:contains(new uploads) > div.gallery > a"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "section.pagination > a.last:not(.disabled)"
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/g/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchMangaByIdParse)
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaByIdParse(response: Response): MangasPage {
|
||||
val details = mangaDetailsParse(response.use { it.asJsoup() })
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.index-container > div.gallery > a"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
thumbnail_url = document.selectFirst("#cover > a > img")?.getImageUrl()
|
||||
|
||||
with(document.selectFirst("div#bigcontainer > div > div#info")!!) {
|
||||
title = selectFirst("h1.title")!!.text()
|
||||
artist = getInfo("Artists")
|
||||
genre = getInfo("Tags")
|
||||
|
||||
description = buildString {
|
||||
select(".title > span").eachText().joinToString("\n").also {
|
||||
append("Full titles:\n$it\n")
|
||||
}
|
||||
|
||||
getInfo("Groups")?.also { append("\nGroups: $it") }
|
||||
getInfo("Languages")?.also { append("\nLanguages: $it") }
|
||||
getInfo("Parodies")?.also { append("\nParodies: $it") }
|
||||
getInfo("Pages")?.also { append("\nPages: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
}
|
||||
|
||||
private fun Element.getInfo(item: String) =
|
||||
select("div.field-name:containsOwn($item) a.tag > span.name")
|
||||
.eachText()
|
||||
.takeUnless { it.isEmpty() }
|
||||
?.joinToString()
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapter = SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1F
|
||||
}
|
||||
|
||||
return Observable.just(listOf(chapter))
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.thumbs a.gallerythumb > img").mapIndexed { index, item ->
|
||||
val url = item.getImageUrl()
|
||||
// Show original images instead of previews
|
||||
val imageUrl = url.substringBeforeLast('/') + "/" + url.substringAfterLast('/').replace("t.", ".")
|
||||
Page(index, "", imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun Element.getImageUrl() = absUrl("data-src").ifEmpty { absUrl("src") }
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
}
|
||||
}
|
|
@ -2,4 +2,15 @@ package eu.kanade.tachiyomi.extension.all.kemono
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
||||
|
||||
class Kemono : Kemono("Kemono", "https://kemono.su", "all")
|
||||
class Kemono : Kemono("Kemono", "https://kemono.su", "all") {
|
||||
override val getTypes = listOf(
|
||||
"Patreon",
|
||||
"Pixiv Fanbox",
|
||||
"Discord",
|
||||
"Fantia",
|
||||
"Afdian",
|
||||
"Boosty",
|
||||
"Gumroad",
|
||||
"SubscribeStar",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Komga'
|
||||
extClass = '.KomgaFactory'
|
||||
extVersionCode = 57
|
||||
extVersionCode = 58
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -251,6 +251,10 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers = headersBuilder().add("Accept", "image/*,*/*;q=0.8").build())
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
fetchFilterOptions()
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
|||
extName = 'Miau Scan'
|
||||
extClass = '.MiauScanFactory'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://lectormiau.com'
|
||||
overrideVersionCode = 4
|
||||
baseUrl = 'https://zonamiau.com'
|
||||
overrideVersionCode = 5
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -20,7 +20,7 @@ class MiauScanFactory : SourceFactory {
|
|||
|
||||
open class MiauScan(lang: String) : MangaThemesia(
|
||||
name = "Miau Scan",
|
||||
baseUrl = "https://lectormiau.com",
|
||||
baseUrl = "https://zonamiau.com",
|
||||
lang = lang,
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MyReadingManga'
|
||||
extClass = '.MyReadingMangaFactory'
|
||||
extVersionCode = 50
|
||||
extVersionCode = 53
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
|||
title = cleanTitle(document.select("h1").text())
|
||||
author = cleanAuthor(document.select("h1").text())
|
||||
artist = author
|
||||
genre = document.select(".entry-header p a[href*=genre]").joinToString { it.text() }
|
||||
genre = document.select(".entry-header p a[href*=genre], [href*=tag], span.entry-categories a").joinToString { it.text() }
|
||||
val basicDescription = document.select("h1").text()
|
||||
// too troublesome to achieve 100% accuracy assigning scanlator group during chapterListParse
|
||||
val scanlatedBy = document.select(".entry-terms:has(a[href*=group])").firstOrNull()
|
||||
|
|
|
@ -23,7 +23,7 @@ private val languageList = listOf(
|
|||
// Source("", "Finnish"),
|
||||
// Source("", "Flemish", "flemish-dutch"),
|
||||
// Source("", "Dutch"),
|
||||
Source("fr", "French"),
|
||||
// Source("fr", "French"),
|
||||
Source("de", "German"),
|
||||
// Source("", "Greek"),
|
||||
// Source("", "Hebrew"),
|
||||
|
|
|
@ -9,7 +9,6 @@ class MyRockMangaFactory : SourceFactory {
|
|||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "vi"),
|
||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "en"),
|
||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "it"),
|
||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "fr"),
|
||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "es"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Photos18'
|
||||
extClass = '.Photos18'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -88,8 +88,8 @@ class Photos18 : HttpSource(), ConfigurableSource {
|
|||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapter = SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = manga.title
|
||||
chapter_number = -2f
|
||||
name = "Gallery"
|
||||
chapter_number = 0f
|
||||
}
|
||||
return Observable.just(listOf(chapter))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Pixiv'
|
||||
extClass = '.PixivFactory'
|
||||
extVersionCode = 8
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ class Pixiv(override val lang: String) : HttpSource() {
|
|||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val filters = filters.list as PixivFilters
|
||||
val hash = Pair(query, filters).hashCode()
|
||||
val hash = Pair(query, filters.toList()).hashCode()
|
||||
|
||||
if (hash != searchHash || page == 1) {
|
||||
searchHash = hash
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.unionmangas.UnionMangasUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="unionmangasbr.org" />
|
||||
|
||||
<data android:scheme="https"/>
|
||||
<data android:pathPattern="/manga-br/..*"/>
|
||||
|
||||
<data android:scheme="https"/>
|
||||
<data android:pathPattern="/italy/..*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,12 +0,0 @@
|
|||
ext {
|
||||
extName = 'Union Mangas'
|
||||
extClass = '.UnionMangasFactory'
|
||||
extVersionCode = 6
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:cryptoaes'))
|
||||
}
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,210 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
|
||||
override val lang = langOption.lang
|
||||
|
||||
override val name: String = "Union Mangas"
|
||||
|
||||
override val baseUrl: String = "https://unionmangasbr.org"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
var currentPage = 0
|
||||
do {
|
||||
val chaptersDto = fetchChapterListPageable(manga, currentPage)
|
||||
chapters += chaptersDto.data.map { chapter ->
|
||||
SChapter.create().apply {
|
||||
name = chapter.name
|
||||
date_upload = chapter.date.toDate()
|
||||
url = chapter.toChapterUrl(langOption.infix)
|
||||
}
|
||||
}
|
||||
currentPage++
|
||||
} while (chaptersDto.hasNextPage())
|
||||
return Observable.just(chapters)
|
||||
}
|
||||
|
||||
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
|
||||
manga.apply {
|
||||
url = getURLCompatibility(url)
|
||||
}
|
||||
|
||||
val maxResult = 16
|
||||
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
|
||||
return client.newCall(GET(url, headers)).execute()
|
||||
.parseAs<Pageable<ChapterDto>>()
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val maxResult = 24
|
||||
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
|
||||
.addPathSegment("$maxResult")
|
||||
.addPathSegment("${page - 1}")
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
manga.apply {
|
||||
url = getURLCompatibility(url)
|
||||
}
|
||||
|
||||
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
manga.apply {
|
||||
url = getURLCompatibility(url)
|
||||
}
|
||||
|
||||
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
|
||||
.addPathSegment(manga.slug())
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val dto = response.parseAs<MangaDetailsDto>()
|
||||
return mangaParse(dto.details)
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterSlug = getURLCompatibility(chapter.url)
|
||||
.substringAfter(langOption.infix)
|
||||
|
||||
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val location = response.request.url.toString()
|
||||
val dto = response.parseAs<PageDto>()
|
||||
return dto.pages.mapIndexed { index, url ->
|
||||
Page(index, location, imageUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val dto = response.parseAs<Pageable<MangaDto>>()
|
||||
val mangas = dto.data.map(::mangaParse)
|
||||
return MangasPage(
|
||||
mangas = mangas,
|
||||
hasNextPage = dto.hasNextPage(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val maxResult = 24
|
||||
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val maxResult = 20
|
||||
val url = "$apiUrl/${langOption.infix}/QuickSearch/".toHttpUrl().newBuilder()
|
||||
.addPathSegment(query)
|
||||
.addPathSegment("$maxResult")
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(SEARCH_PREFIX)) {
|
||||
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
|
||||
return client.newCall(GET(url, headers))
|
||||
.asObservableSuccess().map { response ->
|
||||
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
|
||||
MangasPage(mangas, false)
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val dto = response.parseAs<SearchDto>()
|
||||
return MangasPage(
|
||||
dto.mangas.map(::mangaParse),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Keeps compatibility with pt-BR previous version
|
||||
* */
|
||||
private fun getURLCompatibility(url: String): String {
|
||||
val slugSuffix = "-br"
|
||||
val mangaSubString = "manga-br"
|
||||
|
||||
val oldSlug = url.substringAfter(mangaSubString)
|
||||
.substring(1)
|
||||
.split("/")
|
||||
.first()
|
||||
|
||||
val newSlug = oldSlug.substringBeforeLast(slugSuffix)
|
||||
|
||||
return url.replace(oldSlug, newSlug)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private fun SManga.slug() = this.url.split("/").last()
|
||||
|
||||
private fun mangaParse(dto: MangaDto): SManga {
|
||||
return SManga.create().apply {
|
||||
title = dto.title
|
||||
thumbnail_url = dto.thumbnailUrl
|
||||
status = dto.status
|
||||
url = "/${langOption.infix}/${dto.slug}"
|
||||
genre = dto.genres
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toDate(): Long =
|
||||
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
|
||||
|
||||
companion object {
|
||||
const val SEARCH_PREFIX = "slug:"
|
||||
val apiUrl = "https://api.novelfull.us/api"
|
||||
val oldApiUrl = "https://api.unionmanga.xyz"
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class MangaDetailsDto(private val data: Props) {
|
||||
val details: MangaDto get() = data.details
|
||||
|
||||
@Serializable
|
||||
class Props(
|
||||
@SerialName("infoDoc") val details: MangaDto,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
open class Pageable<T>(
|
||||
var currentPage: Int,
|
||||
var totalPage: Int,
|
||||
val data: List<T>,
|
||||
) {
|
||||
fun hasNextPage() = (currentPage + 1) <= totalPage
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
val date: String,
|
||||
@SerialName("idDoc") val slugManga: String,
|
||||
@SerialName("idDetail") val id: String,
|
||||
@SerialName("nameChapter") val name: String,
|
||||
) {
|
||||
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
@SerialName("name") val title: String,
|
||||
@SerialName("image") private val _thumbnailUrl: String,
|
||||
@SerialName("idDoc") val slug: String,
|
||||
@SerialName("genresName") val genres: String,
|
||||
@SerialName("status") val _status: String,
|
||||
) {
|
||||
val thumbnailUrl get() = "${UnionMangas.oldApiUrl}$_thumbnailUrl"
|
||||
|
||||
val status get() = when (_status) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SearchDto(
|
||||
@SerialName("data")
|
||||
val mangas: List<MangaDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PageDto(val `data`: Data) {
|
||||
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
|
||||
|
||||
@Serializable
|
||||
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
|
||||
|
||||
@Serializable
|
||||
class DetailDocuments(val source: String)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class UnionMangasFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = languages.map { UnionMangas(it) }
|
||||
}
|
||||
|
||||
class LanguageOption(val lang: String, val infix: String = lang, val mangaSubstring: String = infix)
|
||||
|
||||
val languages = listOf(
|
||||
LanguageOption("pt-BR", "manga-br"),
|
||||
LanguageOption("ru", "manga-ru", "mangas"),
|
||||
)
|
|
@ -2,8 +2,9 @@ ext {
|
|||
extName = 'Area Manga'
|
||||
extClass = '.AreaManga'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://www.areascans.net'
|
||||
overrideVersionCode = 0
|
||||
baseUrl = 'https://ar.kenmanga.com'
|
||||
overrideVersionCode = 1
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -6,7 +6,7 @@ import java.util.Locale
|
|||
|
||||
class AreaManga : MangaThemesia(
|
||||
"أريا مانجا",
|
||||
"https://www.areascans.net",
|
||||
"https://ar.kenmanga.com",
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||
)
|
||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
|||
extName = 'MangaNoon'
|
||||
extClass = '.MangaNoon'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://manjanoon.xyz'
|
||||
overrideVersionCode = 5
|
||||
baseUrl = 'https://axztu.com'
|
||||
overrideVersionCode = 7
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.util.Calendar
|
|||
|
||||
class MangaNoon : MangaThemesia(
|
||||
"مانجا نون",
|
||||
"https://manjanoon.xyz",
|
||||
"https://axztu.com",
|
||||
"ar",
|
||||
) {
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
|||
extName = 'MangaSwat'
|
||||
extClass = '.MangaSwat'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://healteer.com'
|
||||
overrideVersionCode = 23
|
||||
baseUrl = 'https://swatscans.com'
|
||||
overrideVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.util.Locale
|
|||
class MangaSwat :
|
||||
MangaThemesia(
|
||||
"MangaSwat",
|
||||
"https://healteer.com",
|
||||
"https://swatscans.com",
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||
),
|
||||
|
|
|
@ -2,4 +2,12 @@ package eu.kanade.tachiyomi.extension.ar.scans4u
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
||||
|
||||
class Scans4u : Keyoapp("Scans 4u", "https://4uscans.com", "ar")
|
||||
class Scans4u : Keyoapp("Scans 4u", "https://4uscans.com", "ar") {
|
||||
|
||||
override fun chapterListSelector(): String {
|
||||
if (!preferences.showPaidChapters) {
|
||||
return "#chapters > a:not(:has(.text-sm span:matches(قادم))):not(:has(img[src*=Coin.svg]))"
|
||||
}
|
||||
return "#chapters > a:not(:has(.text-sm span:matches(قادم)))"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'Toomics.Top'
|
||||
extClass = '.ToomicsTop'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,44 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.de.toomicstop
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import okhttp3.Response
|
||||
|
||||
class ToomicsTop : HotComics(
|
||||
"Toomics.Top",
|
||||
"de",
|
||||
"https://toomics.top",
|
||||
) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangasPage = super.searchMangaParse(response)
|
||||
mangasPage.mangas.apply {
|
||||
for (i in indices) {
|
||||
this[i].url = this[i].url.replace(urlIdRegex, ".html")
|
||||
}
|
||||
}
|
||||
|
||||
return mangasPage
|
||||
}
|
||||
|
||||
private val urlIdRegex = Regex("""(/\w+).html""")
|
||||
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Romantik", "en/genres/Romantik"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: Schulleben", "en/genres/Schulleben"),
|
||||
Pair("Genre: Fantasy", "en/genres/Fantasy"),
|
||||
Pair("Genre: Comedy", "en/genres/Comedy"),
|
||||
Pair("Genre: Historisch", "en/genres/Historisch"),
|
||||
Pair("Genre: Sci-Fi", "en/genres/Sci-Fi"),
|
||||
Pair("Genre: Thriller", "en/genres/Thriller"),
|
||||
Pair("Genre: Horror", "en/genres/Horror"),
|
||||
Pair("Genre: Sport", "en/genres/Sport"),
|
||||
Pair("Genre: GL", "en/genres/GL"),
|
||||
)
|
||||
}
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.AdultWebtoon'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://adultwebtoon.com'
|
||||
overrideVersionCode = 3
|
||||
overrideVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.extension.en.adultwebtoon
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
||||
override val mangaSubString = "adult-webtoon"
|
||||
|
@ -15,15 +13,8 @@ class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
|||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.next"
|
||||
override fun searchMangaSelector() = "li.movie-item > a"
|
||||
override fun searchMangaNextPageSelector() = "a.next"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
title = element.attr("title")
|
||||
}
|
||||
}
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun oldXhrChaptersRequest(mangaId: String): Request {
|
||||
val form = FormBody.Builder()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
ext {
|
||||
extName = 'Arven Scans'
|
||||
extClass = '.ArvenComics'
|
||||
themePkg = 'mangathemesia'
|
||||
themePkg = 'keyoapp'
|
||||
baseUrl = 'https://arvencomics.com'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 24
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package eu.kanade.tachiyomi.extension.en.arvencomics
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
||||
|
||||
class ArvenComics : MangaThemesia(
|
||||
class ArvenComics : Keyoapp(
|
||||
"Arven Scans",
|
||||
"https://arvencomics.com",
|
||||
"en",
|
||||
mangaUrlDirectory = "/series",
|
||||
)
|
||||
) {
|
||||
// migrated from Mangathemesia to Keyoapp
|
||||
override val versionId = 2
|
||||
|
||||
override val cdnUrl = "https://3xfsjdlc.is1.buzz/uploads"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Asura Scans'
|
||||
extClass = '.AsuraScans'
|
||||
extVersionCode = 39
|
||||
extVersionCode = 41
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -57,6 +57,9 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
if (contains("pref_permanent_manga_url_2_en")) {
|
||||
edit().remove("pref_permanent_manga_url_2_en").apply()
|
||||
}
|
||||
if (contains("pref_slug_map")) {
|
||||
edit().remove("pref_slug_map").apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,9 +195,11 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
if (preferences.dynamicUrl()) {
|
||||
val url = response.request.url.toString()
|
||||
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
||||
val absSlug = newSlug.substringBeforeLast("-")
|
||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||
val newSlug = url.substringAfter("/series/", "").substringBefore("/")
|
||||
if (newSlug.isNotEmpty()) {
|
||||
val absSlug = newSlug.substringBeforeLast("-")
|
||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||
}
|
||||
}
|
||||
return super.mangaDetailsParse(response)
|
||||
}
|
||||
|
@ -225,9 +230,11 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (preferences.dynamicUrl()) {
|
||||
val url = response.request.url.toString()
|
||||
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
||||
val absSlug = newSlug.substringBeforeLast("-")
|
||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||
val newSlug = url.substringAfter("/series/", "").substringBefore("/")
|
||||
if (newSlug.isNotEmpty()) {
|
||||
val absSlug = newSlug.substringBeforeLast("-")
|
||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||
}
|
||||
}
|
||||
return super.chapterListParse(response)
|
||||
}
|
||||
|
@ -238,9 +245,9 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href").toPermSlugIfNeeded())
|
||||
name = element.selectFirst("h3:eq(0)")!!.text()
|
||||
name = element.selectFirst("h3")!!.text()
|
||||
date_upload = try {
|
||||
val text = element.selectFirst("h3:eq(1)")!!.ownText()
|
||||
val text = element.selectFirst("h3 + h3")!!.ownText()
|
||||
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
|
||||
dateFormat.parse(cleanText)?.time ?: 0
|
||||
} catch (_: Exception) {
|
||||
|
@ -308,7 +315,7 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
|||
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
|
||||
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
|
||||
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
|
||||
private const val PREF_SLUG_MAP = "pref_slug_map"
|
||||
private const val PREF_SLUG_MAP = "pref_slug_map_2"
|
||||
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'BatCave'
|
||||
extClass = '.BatCave'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 7.0 KiB |
|
@ -0,0 +1,239 @@
|
|||
package eu.kanade.tachiyomi.extension.en.batcave
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BatCave : HttpSource() {
|
||||
|
||||
override val name = "BatCave"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
override val baseUrl = "https://batcave.biz"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
|
||||
addPathSegment(query.trim())
|
||||
if (page > 1) {
|
||||
addPathSegments("page/$page/")
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
var filtersApplied = false
|
||||
|
||||
val url = "$baseUrl/comix/".toHttpUrl().newBuilder().apply {
|
||||
filters.get<YearFilter>()?.addFilterToUrl(this)
|
||||
?.also { filtersApplied = it }
|
||||
filters.get<PublisherFilter>()?.addFilterToUrl(this)
|
||||
?.also { filtersApplied = filtersApplied || it }
|
||||
filters.get<GenreFilter>()?.addFilterToUrl(this)
|
||||
?.also { filtersApplied = filtersApplied || it }
|
||||
|
||||
if (filtersApplied) {
|
||||
setPathSegment(0, "ComicList")
|
||||
}
|
||||
if (page > 1) {
|
||||
addPathSegments("page/$page/")
|
||||
}
|
||||
}.build().toString()
|
||||
|
||||
val sort = filters.get<SortFilter>()!!
|
||||
|
||||
return if (sort.getSort() == "") {
|
||||
GET(url, headers)
|
||||
} else {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("dlenewssortby", sort.getSort())
|
||||
add("dledirection", sort.getDirection())
|
||||
if (filtersApplied) {
|
||||
add("set_new_sort", "dle_sort_xfilter")
|
||||
add("set_direction_sort", "dle_direction_xfilter")
|
||||
} else {
|
||||
add("set_new_sort", "dle_sort_cat_1")
|
||||
add("set_direction_sort", "dle_direction_cat_1")
|
||||
}
|
||||
}.build()
|
||||
|
||||
POST(url, headers, form)
|
||||
}
|
||||
}
|
||||
|
||||
private var publishers: List<Pair<String, Int>> = emptyList()
|
||||
private var genres: List<Pair<String, Int>> = emptyList()
|
||||
private var filterParseFailed = false
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters: MutableList<Filter<*>> = mutableListOf(
|
||||
Filter.Header("Doesn't work with text search"),
|
||||
SortFilter(),
|
||||
YearFilter(),
|
||||
)
|
||||
if (publishers.isNotEmpty()) {
|
||||
filters.add(
|
||||
PublisherFilter(publishers),
|
||||
)
|
||||
}
|
||||
if (genres.isNotEmpty()) {
|
||||
filters.add(
|
||||
GenreFilter(genres),
|
||||
)
|
||||
}
|
||||
if (filters.size < 5) {
|
||||
filters.add(
|
||||
Filter.Header(
|
||||
if (filterParseFailed) {
|
||||
"Unable to load more filters"
|
||||
} else {
|
||||
"Press 'reset' to load more filters"
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
private fun parseFilters(documented: Document) {
|
||||
val script = documented.selectFirst("script:containsData(__XFILTER__)")
|
||||
|
||||
if (script == null) {
|
||||
filterParseFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
val data = try {
|
||||
script.data()
|
||||
.substringAfter("=")
|
||||
.trim()
|
||||
.removeSuffix(";")
|
||||
.parseAs<XFilters>()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e(name, "filters", e)
|
||||
filterParseFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
publishers = data.filterItems.publisher.values.map { it.value to it.id }
|
||||
genres = data.filterItems.genre.values.map { it.value to it.id }
|
||||
filterParseFailed = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
if (response.request.url.pathSegments[0] != "search") {
|
||||
parseFilters(document)
|
||||
}
|
||||
val entries = document.select("#dle-content > .readed").map { element ->
|
||||
SManga.create().apply {
|
||||
with(element.selectFirst(".readed__title > a")!!) {
|
||||
setUrlWithoutDomain(absUrl("href"))
|
||||
title = ownText()
|
||||
}
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
|
||||
}
|
||||
}
|
||||
val hasNextPage = document.selectFirst("div.pagination__pages")
|
||||
?.children()?.last()?.tagName() == "a"
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = document.selectFirst("header.page__header h1")!!.text()
|
||||
thumbnail_url = document.selectFirst("div.page__poster img")?.absUrl("src")
|
||||
description = document.selectFirst("div.page__text")?.wholeText()
|
||||
author = document.selectFirst(".page__list > li:has(> div:contains(Publisher))")?.ownText()
|
||||
status = when (document.selectFirst(".page__list > li:has(> div:contains(release type))")?.ownText()?.trim()) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Complete" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val data = document.selectFirst(".page__chapters-list script:containsData(__DATA__)")!!.data()
|
||||
.substringAfter("=")
|
||||
.trim()
|
||||
.removeSuffix(";")
|
||||
.parseAs<Chapters>()
|
||||
|
||||
return data.chapters.map { chap ->
|
||||
SChapter.create().apply {
|
||||
url = "/reader/${data.comicId}/${chap.id}${data.xhash}"
|
||||
name = chap.title
|
||||
chapter_number = chap.number
|
||||
date_upload = try {
|
||||
dateFormat.parse(chap.date)?.time ?: 0
|
||||
} catch (_: ParseException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val data = document.selectFirst("script:containsData(__DATA__)")!!.data()
|
||||
.substringAfter("=")
|
||||
.trim()
|
||||
.removeSuffix(";")
|
||||
.parseAs<Images>()
|
||||
|
||||
return data.images.mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = baseUrl + img.trim())
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private inline fun <reified T> FilterList.get(): T? {
|
||||
return filterIsInstance<T>().firstOrNull()
|
||||
}
|
||||
|
||||
private inline fun <reified T> String.parseAs(): T {
|
||||
return json.decodeFromString(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package eu.kanade.tachiyomi.extension.en.batcave
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class XFilters(
|
||||
@SerialName("filter_items") val filterItems: XFilterItems = XFilterItems(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class XFilterItems(
|
||||
@SerialName("p") val publisher: XFilterItem = XFilterItem(),
|
||||
@SerialName("g") var genre: XFilterItem = XFilterItem(),
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class XFilterItem(
|
||||
val values: ArrayList<Values> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Values(
|
||||
val id: Int,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Chapters(
|
||||
@SerialName("news_id") val comicId: Int,
|
||||
val chapters: List<Chapter>,
|
||||
val xhash: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Chapter(
|
||||
val id: Int,
|
||||
@SerialName("posi") val number: Float,
|
||||
val title: String,
|
||||
val date: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Images(
|
||||
val images: List<String>,
|
||||
)
|
|
@ -0,0 +1,113 @@
|
|||
package eu.kanade.tachiyomi.extension.en.batcave
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
interface UrlPartFilter {
|
||||
fun addFilterToUrl(url: HttpUrl.Builder): Boolean
|
||||
}
|
||||
|
||||
class CheckBoxItem(name: String, val value: Int) : Filter.CheckBox(name)
|
||||
|
||||
open class CheckBoxFilter(
|
||||
name: String,
|
||||
private val queryParameter: String,
|
||||
values: List<Pair<String, Int>>,
|
||||
) : Filter.Group<CheckBoxItem>(
|
||||
name,
|
||||
values.map { CheckBoxItem(it.first, it.second) },
|
||||
),
|
||||
UrlPartFilter {
|
||||
override fun addFilterToUrl(url: HttpUrl.Builder): Boolean {
|
||||
val checked = state.filter { it.state }
|
||||
.also { if (it.isEmpty()) return false }
|
||||
.joinToString(",") { it.value.toString() }
|
||||
|
||||
url.addPathSegments("$queryParameter=$checked/")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class PublisherFilter(values: List<Pair<String, Int>>) :
|
||||
CheckBoxFilter("Publisher", "p", values)
|
||||
|
||||
class GenreFilter(values: List<Pair<String, Int>>) :
|
||||
CheckBoxFilter("Genre", "g", values)
|
||||
|
||||
class TextBox(name: String) : Filter.Text(name)
|
||||
|
||||
class YearFilter :
|
||||
Filter.Group<TextBox>(
|
||||
"Year of Issue",
|
||||
listOf(
|
||||
TextBox("from"),
|
||||
TextBox("to"),
|
||||
),
|
||||
),
|
||||
UrlPartFilter {
|
||||
override fun addFilterToUrl(url: HttpUrl.Builder): Boolean {
|
||||
var applied = false
|
||||
val currentYear = yearFormat.format(Date()).toInt()
|
||||
if (state[0].state.isNotBlank()) {
|
||||
val from = try {
|
||||
state[0].state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
throw Exception("year must be number")
|
||||
}
|
||||
assert(from in 1929..currentYear) {
|
||||
"invalid start year (must be between 1929 and $currentYear)"
|
||||
}
|
||||
url.addPathSegments("y[from]=$from/")
|
||||
applied = true
|
||||
}
|
||||
if (state[1].state.isNotBlank()) {
|
||||
val to = try {
|
||||
state[1].state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
throw Exception("year must be number")
|
||||
}
|
||||
assert(to in 1929..currentYear) {
|
||||
"invalid start year (must be between 1929 and $currentYear)"
|
||||
}
|
||||
url.addPathSegments("y[to]=$to/")
|
||||
applied = true
|
||||
}
|
||||
return applied
|
||||
}
|
||||
}
|
||||
|
||||
private val yearFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
|
||||
|
||||
class SortFilter(
|
||||
select: Selection = Selection(0, false),
|
||||
) : Filter.Sort(
|
||||
"Sort",
|
||||
sorts.map { it.first }.toTypedArray(),
|
||||
select,
|
||||
) {
|
||||
fun getSort() = sorts[state?.index ?: 0].second
|
||||
fun getDirection() = if (state?.ascending != false) {
|
||||
"asc"
|
||||
} else {
|
||||
"desc"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val POPULAR = FilterList(SortFilter(Selection(3, false)))
|
||||
val LATEST = FilterList(SortFilter(Selection(2, false)))
|
||||
}
|
||||
}
|
||||
|
||||
private val sorts = listOf(
|
||||
"Default" to "",
|
||||
"Date" to "date",
|
||||
"Date of change" to "editdate",
|
||||
"Rating" to "rating",
|
||||
"Read" to "news_read",
|
||||
"Comments" to "comm_num",
|
||||
"Title" to "title",
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
ext {
|
||||
extName = 'Comic1000'
|
||||
extClass = '.Comic1000'
|
||||
themePkg = 'manga18'
|
||||
baseUrl = 'https://comic1000.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,5 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.comic1000
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.manga18.Manga18
|
||||
|
||||
class Comic1000 : Manga18("Comic1000", "https://comic1000.com", "en")
|
|
@ -2,8 +2,9 @@ ext {
|
|||
extName = 'Drake Scans'
|
||||
extClass = '.DrakeScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://drakecomic.com'
|
||||
overrideVersionCode = 13
|
||||
baseUrl = 'https://drakecomic.org'
|
||||
overrideVersionCode = 14
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|