Compare commits
No commits in common. "cb2794830738e740114e15a59b50655413e0f059" and "e0bb39f99adb84553c28dce5f190fbeb60fb80b9" have entirely different histories.
cb27948307
...
e0bb39f99a
|
@ -86,9 +86,12 @@ small, just do a normal full clone instead.**
|
||||||
```bash
|
```bash
|
||||||
git sparse-checkout set --cone --sparse-index
|
git sparse-checkout set --cone --sparse-index
|
||||||
# add project folders
|
# add project folders
|
||||||
git sparse-checkout add buildSrc core gradle lib lib-multisrc
|
git sparse-checkout add .run buildSrc core gradle lib multisrc/src/main/java/generator
|
||||||
# add a single source
|
# add a single source
|
||||||
git sparse-checkout add src/<lang>/<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
|
To remove a source, open `.git/info/sparse-checkout` and delete the exact
|
||||||
|
@ -109,11 +112,13 @@ small, just do a normal full clone instead.**
|
||||||
```bash
|
```bash
|
||||||
/*
|
/*
|
||||||
!/src/*
|
!/src/*
|
||||||
!/multisrc-lib/*
|
!/multisrc/overrides/*
|
||||||
|
!/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/*
|
||||||
# allow a single source
|
# allow a single source
|
||||||
/src/<lang>/<source>
|
/src/<lang>/<source>
|
||||||
# allow a multisrc theme
|
# allow a multisrc theme
|
||||||
/lib-multisrc/<source>
|
/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/<source>
|
||||||
|
/multisrc/overrides/<source>
|
||||||
# or type the source name directly
|
# or type the source name directly
|
||||||
<source>
|
<source>
|
||||||
```
|
```
|
||||||
|
@ -837,15 +842,6 @@ of `mitmweb`.
|
||||||
APKs can be created in Android Studio via `Build > Build Bundle(s) / APK(s) > Build APK(s)` or
|
APKs can be created in Android Studio via `Build > Build Bundle(s) / APK(s) > Build APK(s)` or
|
||||||
`Build > Generate Signed Bundle / APK`.
|
`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
|
## Submitting the changes
|
||||||
|
|
||||||
When you feel confident about your changes, submit a new Pull Request so your code can be reviewed
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -4,7 +4,7 @@ coroutines_version = "1.6.4"
|
||||||
serialization_version = "1.4.0"
|
serialization_version = "1.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.6.1" }
|
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.4.1" }
|
||||||
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
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-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
||||||
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
@ -57,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
@ -86,8 +84,7 @@ done
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
|
@ -13,8 +13,6 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.blogtruyen
|
package eu.kanade.tachiyomi.multisrc.blogtruyen
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
@ -367,11 +366,7 @@ abstract class BlogTruyen(
|
||||||
)
|
)
|
||||||
|
|
||||||
Single.fromCallable {
|
Single.fromCallable {
|
||||||
try {
|
|
||||||
client.newCall(request).execute().close()
|
client.newCall(request).execute().close()
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("BlogTruyen", "Error updating view count", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 1
|
||||||
|
|
|
@ -39,7 +39,7 @@ open class GoDa(
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup().also(::parseGenres)
|
val document = response.asJsoup().also(::parseGenres)
|
||||||
val mangas = document.select(".container > .cardlist .pb-2 a").map { element ->
|
val mangas = document.select(".cardlist .pb-2 a").map { element ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
val imgSrc = element.selectFirst("img")!!.attr("src")
|
val imgSrc = element.selectFirst("img")!!.attr("src")
|
||||||
url = getKey(element.attr("href"))
|
url = getKey(element.attr("href"))
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 1
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cookieinterceptor"))
|
api(project(":lib:cookieinterceptor"))
|
||||||
|
|
|
@ -120,7 +120,7 @@ abstract class HotComics(
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
return response.asJsoup().select("#tab-chapter a").map { element ->
|
return response.asJsoup().select("#tab-chapter a").map { element ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("onclick").substringAfter("popupLogin('").substringBefore("'"))
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
name = element.selectFirst(".cell-num")!!.text()
|
name = element.selectFirst(".cell-num")!!.text()
|
||||||
date_upload = parseDate(element.selectFirst(".cell-time")?.text())
|
date_upload = parseDate(element.selectFirst(".cell-time")?.text())
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 14
|
baseVersionCode = 12
|
||||||
|
|
|
@ -4,26 +4,29 @@ import android.app.Application
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.kemono.KemonoCreatorDto.Companion.serviceName
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.blackholeSink
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.lang.Thread.sleep
|
import java.io.IOException
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@ -48,8 +51,6 @@ open class Kemono(
|
||||||
|
|
||||||
private val imgCdnUrl = baseUrl.replace("//", "//img.")
|
private val imgCdnUrl = baseUrl.replace("//", "//img.")
|
||||||
|
|
||||||
private var mangasCache: List<KemonoCreatorDto> = emptyList()
|
|
||||||
|
|
||||||
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
|
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
@ -62,123 +63,82 @@ open class Kemono(
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
searchMangas(page, sortBy = "pop" to "desc")
|
fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
searchMangas(page, sortBy = "lat" to "desc")
|
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) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
||||||
searchMangas(page, query, filters)
|
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 }
|
||||||
}
|
}
|
||||||
|
result
|
||||||
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 -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mangas = mangasCache
|
private fun fetchCreatorsPage(
|
||||||
if (page == 1) {
|
page: Int,
|
||||||
var favourites: List<KemonoFavouritesDto> = emptyList()
|
block: (ArrayList<KemonoCreatorDto>) -> List<KemonoCreatorDto>,
|
||||||
if (fav != null) {
|
): MangasPage {
|
||||||
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
val imgCdnUrl = this.imgCdnUrl
|
||||||
|
|
||||||
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 response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
|
||||||
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
|
val allCreators = block(response.parseAs())
|
||||||
mangas = allCreators.filter {
|
val count = allCreators.size
|
||||||
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
|
val fromIndex = (page - 1) * NEW_PAGE_SIZE
|
||||||
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
|
val toIndex = min(count, fromIndex + NEW_PAGE_SIZE)
|
||||||
|
val creators = allCreators.subList(fromIndex, toIndex)
|
||||||
val regularSearch = it.name.contains(title, true)
|
.map { it.toSManga(imgCdnUrl) }
|
||||||
|
.filterUnsupported()
|
||||||
val isFavourited = when (fav) {
|
return MangasPage(creators, toIndex < count)
|
||||||
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
|
|
||||||
false -> favourites.none { f -> f.id == it.id }
|
|
||||||
else -> true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
includeType && !excludeType && isFavourited &&
|
private fun cacheCreators() {
|
||||||
regularSearch
|
val callback = object : Callback {
|
||||||
}.also { mangasCache = mangas }
|
override fun onResponse(call: Call, response: Response) =
|
||||||
|
response.body.source().run {
|
||||||
|
readAll(blackholeSink())
|
||||||
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sorted = when (sort.first) {
|
override fun onFailure(call: Call, e: IOException) = Unit
|
||||||
"pop" -> {
|
|
||||||
if (sort.second == "desc") {
|
|
||||||
mangas.sortedByDescending { it.favorited }
|
|
||||||
} else {
|
|
||||||
mangas.sortedBy { it.favorited }
|
|
||||||
}
|
}
|
||||||
}
|
client.newCall(GET("$baseUrl/$apiPath/creators", headers)).enqueue(callback)
|
||||||
"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()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
|
@ -191,38 +151,33 @@ open class Kemono(
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
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 {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||||
KemonoPostDto.dateFormat.timeZone = when (manga.author) {
|
KemonoPostDto.dateFormat.timeZone = when (manga.author) {
|
||||||
"Pixiv Fanbox", "Fantia" -> TimeZone.getTimeZone("GMT+09:00")
|
"Pixiv Fanbox", "Fantia" -> TimeZone.getTimeZone("GMT+09:00")
|
||||||
else -> TimeZone.getTimeZone("GMT")
|
else -> TimeZone.getTimeZone("GMT")
|
||||||
}
|
}
|
||||||
val prefMaxPost = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
|
val maxPosts = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
|
||||||
.toInt().coerceAtMost(POST_PAGES_MAX) * PAGE_POST_LIMIT
|
.toInt().coerceAtMost(POST_PAGES_MAX) * POST_PAGE_SIZE
|
||||||
var offset = 0
|
var offset = 0
|
||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
val result = ArrayList<SChapter>()
|
val result = ArrayList<SChapter>()
|
||||||
while (offset < prefMaxPost && hasNextPage) {
|
while (offset < maxPosts && hasNextPage) {
|
||||||
val request = GET("$baseUrl/$apiPath${manga.url}?o=$offset", headers)
|
val request = GET("$baseUrl/$apiPath${manga.url}?limit=$POST_PAGE_SIZE&o=$offset", headers)
|
||||||
val page: List<KemonoPostDto> = retry(request).parseAs()
|
val page: List<KemonoPostDto> = retry(request).parseAs()
|
||||||
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
|
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
|
||||||
offset += PAGE_POST_LIMIT
|
offset += POST_PAGE_SIZE
|
||||||
hasNextPage = page.size == PAGE_POST_LIMIT
|
hasNextPage = page.size == POST_PAGE_SIZE
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retry(request: Request): Response {
|
private fun retry(request: Request): Response {
|
||||||
var code = 0
|
var code = 0
|
||||||
repeat(5) {
|
repeat(3) {
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
if (response.isSuccessful) return response
|
if (response.isSuccessful) return response
|
||||||
response.close()
|
response.close()
|
||||||
code = response.code
|
code = response.code
|
||||||
if (code == 429) {
|
|
||||||
sleep(10000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw Exception("HTTP error $code")
|
throw Exception("HTTP error $code")
|
||||||
}
|
}
|
||||||
|
@ -262,8 +217,10 @@ open class Kemono(
|
||||||
key = POST_PAGES_PREF
|
key = POST_PAGES_PREF
|
||||||
title = "Maximum posts to load"
|
title = "Maximum posts to load"
|
||||||
summary = "Loading more posts costs more time and network traffic.\nCurrently: %s"
|
summary = "Loading more posts costs more time and network traffic.\nCurrently: %s"
|
||||||
entryValues = Array(POST_PAGES_MAX) { (it + 1).toString() }
|
entryValues = (1..POST_PAGES_MAX).map { it.toString() }.toTypedArray()
|
||||||
entries = Array(POST_PAGES_MAX) { "${(it + 1)} pages (${(it + 1) * PAGE_POST_LIMIT} posts)" }
|
entries = (1..POST_PAGES_MAX).map {
|
||||||
|
if (it == 1) "1 page ($POST_PAGE_SIZE posts)" else "$it pages (${it * POST_PAGE_SIZE} posts)"
|
||||||
|
}.toTypedArray()
|
||||||
setDefaultValue(POST_PAGES_DEFAULT)
|
setDefaultValue(POST_PAGES_DEFAULT)
|
||||||
}.let { screen.addPreference(it) }
|
}.let { screen.addPreference(it) }
|
||||||
|
|
||||||
|
@ -275,55 +232,16 @@ open class Kemono(
|
||||||
}.let(screen::addPreference)
|
}.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 {
|
companion object {
|
||||||
private const val PAGE_POST_LIMIT = 50
|
private const val NEW_PAGE_SIZE = 50
|
||||||
private const val PAGE_CREATORS_LIMIT = 50
|
|
||||||
const val PROMPT = "You can change how many posts to load in the extension preferences."
|
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_PREF = "POST_PAGES"
|
||||||
private const val POST_PAGES_DEFAULT = "1"
|
private const val POST_PAGES_DEFAULT = "1"
|
||||||
private const val POST_PAGES_MAX = 75
|
private const val POST_PAGES_MAX = 50
|
||||||
|
|
||||||
|
private fun List<SManga>.filterUnsupported() = filterNot { it.author == "Discord" }
|
||||||
|
|
||||||
// private const val BASE_URL_PREF = "BASE_URL"
|
// private const val BASE_URL_PREF = "BASE_URL"
|
||||||
private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG"
|
private const val USE_LOW_RES_IMG = "USE_LOW_RES_IMG"
|
||||||
|
|
|
@ -7,23 +7,15 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.double
|
import kotlinx.serialization.json.double
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@Serializable
|
|
||||||
class KemonoFavouritesDto(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val service: String,
|
|
||||||
val faved_seq: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoCreatorDto(
|
class KemonoCreatorDto(
|
||||||
val id: String,
|
private val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val service: String,
|
private val service: String,
|
||||||
private val updated: JsonPrimitive,
|
private val updated: JsonPrimitive,
|
||||||
val favorited: Int = -1,
|
val favorited: Int = -1,
|
||||||
) {
|
) {
|
||||||
var fav: Long = 0
|
|
||||||
val updatedDate get() = when {
|
val updatedDate get() = when {
|
||||||
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
|
||||||
else -> (updated.double * 1000).toLong()
|
else -> (updated.double * 1000).toLong()
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
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,8 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 8
|
baseVersionCode = 7
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":lib:i18n"))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.keyoapp
|
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.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -23,8 +17,6 @@ import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -35,12 +27,7 @@ abstract class Keyoapp(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
final override val lang: String,
|
final override val lang: String,
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
@ -52,13 +39,6 @@ abstract class Keyoapp(
|
||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
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
|
// Popular
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||||
|
@ -238,12 +218,7 @@ abstract class Keyoapp(
|
||||||
|
|
||||||
// Chapter list
|
// Chapter list
|
||||||
|
|
||||||
override fun chapterListSelector(): String {
|
override fun chapterListSelector(): String = "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
|
||||||
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 {
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||||
|
@ -251,9 +226,6 @@ abstract class Keyoapp(
|
||||||
element.selectFirst(".text-xs")?.run {
|
element.selectFirst(".text-xs")?.run {
|
||||||
date_upload = text().trim().parseDate()
|
date_upload = text().trim().parseDate()
|
||||||
}
|
}
|
||||||
if (element.select("img[src*=Coin.svg]").isNotEmpty()) {
|
|
||||||
name = "🔒 $name"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image list
|
// Image list
|
||||||
|
@ -263,7 +235,7 @@ abstract class Keyoapp(
|
||||||
.map { it.attr("uid") }
|
.map { it.attr("uid") }
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.mapIndexed { index, img ->
|
.mapIndexed { index, img ->
|
||||||
Page(index, document.location(), "$cdnUrl/$img")
|
Page(index, document.location(), "$cdnUrl/uploads/$img")
|
||||||
}
|
}
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
?.also { return it }
|
?.also { return it }
|
||||||
|
@ -277,7 +249,7 @@ abstract class Keyoapp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open val cdnUrl = "https://2xffbs-cn8.is1.buzz/uploads"
|
protected val cdnUrl = "https://cdn.igniscans.com"
|
||||||
|
|
||||||
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
||||||
|
|
||||||
|
@ -343,22 +315,4 @@ abstract class Keyoapp(
|
||||||
}
|
}
|
||||||
return now.timeInMillis
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 1
|
|
|
@ -1,143 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 32
|
baseVersionCode = 31
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
@ -103,18 +102,18 @@ abstract class LibGroup(
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _constants: Constants? = null
|
private var _constants: Constants? = null
|
||||||
private fun getConstants(): Constants? {
|
private fun getConstants(): Constants {
|
||||||
if (_constants == null) {
|
if (_constants == null) {
|
||||||
try {
|
try {
|
||||||
_constants = client.newCall(
|
_constants = client.newCall(
|
||||||
GET("$apiDomain/api/constants?fields[]=genres&fields[]=tags&fields[]=types&fields[]=scanlateStatus&fields[]=status&fields[]=format&fields[]=ageRestriction&fields[]=imageServers", headers),
|
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
|
).execute().parseAs<Data<Constants>>().data
|
||||||
return _constants
|
return _constants!!
|
||||||
} catch (ex: Exception) {
|
} catch (ex: SerializationException) {
|
||||||
Log.d("LibGroup", "Error getting constants: $ex")
|
throw Exception("Ошибка сериализации. Проверьте сайт.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _constants
|
return _constants!!
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkForToken(chain: Interceptor.Chain): Response {
|
private fun checkForToken(chain: Interceptor.Chain): Response {
|
||||||
|
@ -377,7 +376,7 @@ abstract class LibGroup(
|
||||||
if (page.imageUrl != null) {
|
if (page.imageUrl != null) {
|
||||||
return Observable.just(page.imageUrl)
|
return Observable.just(page.imageUrl)
|
||||||
}
|
}
|
||||||
val server = getConstants()?.getServer(isServer(), siteId)?.url ?: throw Exception("Ошибка получения сервера изображений")
|
val server = getConstants().getServer(isServer(), siteId).url
|
||||||
return Observable.just("$server${page.url}")
|
return Observable.just("$server${page.url}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,13 +508,13 @@ abstract class LibGroup(
|
||||||
|
|
||||||
filters += if (_constants != null) {
|
filters += if (_constants != null) {
|
||||||
listOf(
|
listOf(
|
||||||
CategoryList(getConstants()!!.getCategories(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()) }),
|
FormatList(getConstants().getFormats(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||||
GenreList(getConstants()!!.getGenres(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()) }),
|
TagList(getConstants().getTags(siteId).map { SearchFilter(it.name, it.id.toString()) }),
|
||||||
StatusList(getConstants()!!.getScanlateStatuses(siteId).map { CheckFilter(it.label, 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()) }),
|
StatusTitleList(getConstants().getTitleStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||||
AgeList(getConstants()!!.getAgeRestrictions(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
AgeList(getConstants().getAgeRestrictions(siteId).map { CheckFilter(it.label, it.id.toString()) }),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
@ -42,10 +42,6 @@ abstract class MangaEsp(
|
||||||
classLoader = this::class.java.classLoader!!,
|
classLoader = this::class.java.classLoader!!,
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open val apiPath = "/api"
|
|
||||||
|
|
||||||
protected open val seriesPath = "/ver"
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
override val client: OkHttpClient = network.client.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
.build()
|
.build()
|
||||||
|
@ -53,7 +49,7 @@ abstract class MangaEsp(
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl$apiPath/topSerie", headers)
|
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/api/topSerie", headers)
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val responseData = json.decodeFromString<TopSeriesDto>(response.body.string())
|
val responseData = json.decodeFromString<TopSeriesDto>(response.body.string())
|
||||||
|
@ -62,17 +58,17 @@ abstract class MangaEsp(
|
||||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||||
|
|
||||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga(seriesPath) }
|
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga() }
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl$apiPath/lastUpdates", headers)
|
override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl/api/lastUpdates", headers)
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
||||||
|
|
||||||
val mangas = responseData.response.map { it.toSManga(seriesPath) }
|
val mangas = responseData.response.map { it.toSManga() }
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
@ -155,7 +151,7 @@ abstract class MangaEsp(
|
||||||
|
|
||||||
return MangasPage(
|
return MangasPage(
|
||||||
filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size))
|
filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size))
|
||||||
.map { it.toSManga(seriesPath) },
|
.map { it.toSManga() },
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -175,7 +171,7 @@ abstract class MangaEsp(
|
||||||
?: throw Exception(intl["comic_data_error"])
|
?: throw Exception(intl["comic_data_error"])
|
||||||
val unescapedJson = mangaDetailsJson.unescape()
|
val unescapedJson = mangaDetailsJson.unescape()
|
||||||
val series = json.decodeFromString<SeriesDto>(unescapedJson)
|
val series = json.decodeFromString<SeriesDto>(unescapedJson)
|
||||||
return series.chapters.map { it.toSChapter(seriesPath, series.slug) }
|
return series.chapters.map { it.toSChapter(series.slug) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
|
|
@ -48,11 +48,11 @@ class SeriesDto(
|
||||||
@SerialName("idioma")
|
@SerialName("idioma")
|
||||||
val language: String? = null,
|
val language: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toSManga(seriesPath: String): SManga {
|
fun toSManga(): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
title = name
|
title = name
|
||||||
thumbnail_url = thumbnail
|
thumbnail_url = thumbnail
|
||||||
url = "$seriesPath/$slug"
|
url = "/ver/$slug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ class ChapterDto(
|
||||||
private val slug: String,
|
private val slug: String,
|
||||||
@SerialName("created_at") private val date: String,
|
@SerialName("created_at") private val date: String,
|
||||||
) {
|
) {
|
||||||
fun toSChapter(seriesPath: String, seriesSlug: String): SChapter {
|
fun toSChapter(seriesSlug: String): SChapter {
|
||||||
return SChapter.create().apply {
|
return SChapter.create().apply {
|
||||||
name = "Capítulo ${number.toString().removeSuffix(".0")}"
|
name = "Capítulo ${number.toString().removeSuffix(".0")}"
|
||||||
if (!this@ChapterDto.name.isNullOrBlank()) {
|
if (!this@ChapterDto.name.isNullOrBlank()) {
|
||||||
|
@ -115,7 +115,7 @@ class ChapterDto(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
url = "$seriesPath/$seriesSlug/$slug"
|
url = "/ver/$seriesSlug/$slug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,11 @@ open class MCCMSConfig(
|
||||||
hasCategoryPage: Boolean = true,
|
hasCategoryPage: Boolean = true,
|
||||||
val textSearchOnlyPageOne: Boolean = false,
|
val textSearchOnlyPageOne: Boolean = false,
|
||||||
val useMobilePageList: Boolean = false,
|
val useMobilePageList: Boolean = false,
|
||||||
private val lazyLoadImageAttr: String = "data-original",
|
protected val lazyLoadImageAttr: String = "data-original",
|
||||||
) {
|
) {
|
||||||
val genreData = GenreData(hasCategoryPage)
|
val genreData = GenreData(hasCategoryPage)
|
||||||
|
|
||||||
fun pageListParse(response: Response): List<Page> {
|
open fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
return if (useMobilePageList) {
|
return if (useMobilePageList) {
|
||||||
|
|
|
@ -131,8 +131,8 @@ abstract class WPComics(
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun String?.toStatus(): Int {
|
open fun String?.toStatus(): Int {
|
||||||
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "Đang cập nhật", "連載中")
|
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "連載中")
|
||||||
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "Đã hoàn thành", "完結済み")
|
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "完結済み")
|
||||||
return when {
|
return when {
|
||||||
this == null -> SManga.UNKNOWN
|
this == null -> SManga.UNKNOWN
|
||||||
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
||||||
|
|
|
@ -114,7 +114,7 @@ abstract class ZeistManga(
|
||||||
val result = json.decodeFromString<ZeistMangaDto>(jsonString)
|
val result = json.decodeFromString<ZeistMangaDto>(jsonString)
|
||||||
|
|
||||||
val mangas = result.feed?.entry.orEmpty()
|
val mangas = result.feed?.entry.orEmpty()
|
||||||
.filter { it.category.orEmpty().any { category -> category.term == mangaCategory } }
|
.filter { it.category.orEmpty().any { category -> category.term == "Series" } } // Default category for all series
|
||||||
.filterNot { it.category.orEmpty().any { category -> excludedCategories.contains(category.term) } }
|
.filterNot { it.category.orEmpty().any { category -> excludedCategories.contains(category.term) } }
|
||||||
.map { it.toSManga(baseUrl) }
|
.map { it.toSManga(baseUrl) }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 41
|
extVersionCode = 37
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ open class BatoTo(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val name: String = "Bato.to"
|
override val name: String = "Bato.to"
|
||||||
override val baseUrl: String by lazy { getMirrorPref()!! }
|
override val baseUrl: String = getMirrorPref()!!
|
||||||
override val id: Long = when (lang) {
|
override val id: Long = when (lang) {
|
||||||
"zh-Hans" -> 2818874445640189582
|
"zh-Hans" -> 2818874445640189582
|
||||||
"zh-Hant" -> 38886079663327225
|
"zh-Hant" -> 38886079663327225
|
||||||
|
@ -88,25 +88,12 @@ open class BatoTo(
|
||||||
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
|
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(mirrorPref)
|
||||||
screen.addPreference(altChapterListPref)
|
screen.addPreference(altChapterListPref)
|
||||||
screen.addPreference(removeOfficialPref)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
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 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
|
override val supportsLatest = true
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
@ -322,34 +309,23 @@ open class BatoTo(
|
||||||
}
|
}
|
||||||
return super.mangaDetailsRequest(manga)
|
return super.mangaDetailsRequest(manga)
|
||||||
}
|
}
|
||||||
private var titleRegex: Regex =
|
|
||||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|/.+?)\\s*|([|/~].*)|-.*-")
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val infoElement = document.select("div#mainer div.container-fluid")
|
val infoElement = document.select("div#mainer div.container-fluid")
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||||
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
manga.title = 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.author = infoElement.select("div.attr-item:contains(author) span").text()
|
||||||
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
|
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
|
||||||
manga.status = parseStatus(workStatus, uploadStatus)
|
manga.status = parseStatus(workStatus, uploadStatus)
|
||||||
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
||||||
manga.description = description +
|
manga.description = infoElement.select("div.limit-html").text() + "\n" + infoElement.select(".episode-list > .alert-warning").text().trim()
|
||||||
if (alternativeTitles.isNotBlank()) "\n\nAlternative Titles:\n$alternativeTitles" else ""
|
manga.thumbnail_url = document.select("div.attr-cover img")
|
||||||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
.attr("abs:src")
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
|
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
|
||||||
workStatus == null -> SManga.UNKNOWN
|
workStatus == null -> SManga.UNKNOWN
|
||||||
workStatus.contains("Ongoing") -> SManga.ONGOING
|
workStatus.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
@ -969,7 +945,6 @@ open class BatoTo(
|
||||||
companion object {
|
companion object {
|
||||||
private const val MIRROR_PREF_KEY = "MIRROR"
|
private const val MIRROR_PREF_KEY = "MIRROR"
|
||||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
|
||||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||||
"bato.to",
|
"bato.to",
|
||||||
"batocomic.com",
|
"batocomic.com",
|
||||||
|
@ -987,8 +962,6 @@ open class BatoTo(
|
||||||
"readtoto.net",
|
"readtoto.net",
|
||||||
"readtoto.org",
|
"readtoto.org",
|
||||||
"dto.to",
|
"dto.to",
|
||||||
"fto.to",
|
|
||||||
"jto.to",
|
|
||||||
"hto.to",
|
"hto.to",
|
||||||
"mto.to",
|
"mto.to",
|
||||||
"wto.to",
|
"wto.to",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
extClass = '.ComickFactory'
|
extClass = '.ComickFactory'
|
||||||
extVersionCode = 48
|
extVersionCode = 47
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -329,11 +329,7 @@ abstract class Comick(
|
||||||
is TagFilter -> {
|
is TagFilter -> {
|
||||||
if (it.state.isNotEmpty()) {
|
if (it.state.isNotEmpty()) {
|
||||||
it.state.split(",").forEach {
|
it.state.split(",").forEach {
|
||||||
addQueryParameter(
|
addQueryParameter("tags", it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-").replace("'-", "-and-039-").replace("'", "-and-039-"))
|
||||||
"tags",
|
|
||||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
|
||||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,26 +372,29 @@ abstract class Comick(
|
||||||
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
||||||
val mangaData = response.parseAs<Manga>()
|
val mangaData = response.parseAs<Manga>()
|
||||||
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
|
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 =
|
val coversUrl =
|
||||||
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
|
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
|
||||||
val covers = client.newCall(GET(coversUrl)).execute()
|
val covers = client.newCall(GET(coversUrl)).execute()
|
||||||
.parseAs<Covers>().mdCovers.reversed().toMutableList()
|
.parseAs<Covers>().mdCovers.reversed()
|
||||||
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(
|
return mangaData.toSManga(
|
||||||
includeMuTags = preferences.includeMuTags,
|
includeMuTags = preferences.includeMuTags,
|
||||||
scorePosition = preferences.scorePosition,
|
covers = if (covers.any { it.vol == "1" }) covers.filter { it.vol == "1" } else covers,
|
||||||
covers = covers,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return mangaData.toSManga(
|
return mangaData.toSManga(includeMuTags = preferences.includeMuTags)
|
||||||
includeMuTags = preferences.includeMuTags,
|
|
||||||
scorePosition = preferences.scorePosition,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
@ -512,12 +511,12 @@ abstract class Comick(
|
||||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
private const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||||
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
|
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
|
||||||
private const val FIRST_COVER_PREF = "DefaultCover"
|
private const val FIRST_COVER_PREF = "DefaultCover"
|
||||||
private const val FIRST_COVER_DEFAULT = true
|
private const val FIRST_COVER_DEFAULT = true
|
||||||
private const val SCORE_POSITION_PREF = "ScorePosition"
|
private const val SCORE_POSITION_PREF = "ScorePosition"
|
||||||
const val SCORE_POSITION_DEFAULT = "top"
|
private const val SCORE_POSITION_DEFAULT = "top"
|
||||||
private const val LIMIT = 20
|
private const val LIMIT = 20
|
||||||
private const val CHAPTERS_LIMIT = 99999
|
private const val CHAPTERS_LIMIT = 99999
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
|
@ -33,8 +31,8 @@ class Manga(
|
||||||
private val demographic: String? = null,
|
private val demographic: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toSManga(
|
fun toSManga(
|
||||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
includeMuTags: Boolean = false,
|
||||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
scorePosition: String = "",
|
||||||
covers: List<MDcovers>? = null,
|
covers: List<MDcovers>? = null,
|
||||||
) =
|
) =
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
|
@ -150,7 +148,6 @@ class Covers(
|
||||||
class MDcovers(
|
class MDcovers(
|
||||||
val b2key: String?,
|
val b2key: String?,
|
||||||
val vol: String? = null,
|
val vol: String? = null,
|
||||||
val locale: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -2,10 +2,4 @@ package eu.kanade.tachiyomi.extension.all.coomer
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
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 {
|
ext {
|
||||||
extName = 'CosplayTele'
|
extName = 'CosplayTele'
|
||||||
extClass = '.CosplayTele'
|
extClass = '.CosplayTele'
|
||||||
extVersionCode = 3
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,17 +66,17 @@ class CosplayTele : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val popularPageLimit = 20
|
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 popularMangaSelector(): String = ""
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val respObject = json.decodeFromString<JsonArray>(response.body.string())
|
val jsonObject = json.decodeFromString<JsonArray>(response.body.string())
|
||||||
val mangas = respObject.map { item ->
|
val mangas = jsonObject.map { item ->
|
||||||
|
val head = item.jsonObject["yoast_head_json"]!!.jsonObject
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = item.jsonObject!!["title"]!!.jsonObject!!["rendered"]!!.jsonPrimitive.content
|
title = head["og_title"]!!.jsonPrimitive.content
|
||||||
thumbnail_url = item.jsonObject!!["_embedded"]!!.jsonObject!!["wp:featuredmedia"]!!.jsonArray[0]!!.jsonObject["source_url"]!!.jsonPrimitive.content
|
thumbnail_url = head["og_image"]!!.jsonArray[0].jsonObject["url"]!!.jsonPrimitive.content
|
||||||
setUrlWithoutDomain(item.jsonObject!!["link"]!!.jsonPrimitive.content)
|
setUrlWithoutDomain(head["og_url"]!!.jsonPrimitive.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MangasPage(mangas, mangas.size >= popularPageLimit)
|
return MangasPage(mangas, mangas.size >= popularPageLimit)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Springboard that accepts https://e-hentai.org/g/xxxxx/yyyyy/ intents and redirects them to
|
* Springboard that accepts https://e-hentai.net/g/xxxxx/yyyyy/ intents and redirects them to
|
||||||
* the main Tachiyomi process.
|
* the main Tachiyomi process.
|
||||||
*/
|
*/
|
||||||
class EHUrlActivity : Activity() {
|
class EHUrlActivity : Activity() {
|
||||||
|
|
|
@ -32,14 +32,14 @@ open class EternalMangas(
|
||||||
|
|
||||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||||
.filter { it.language == internalLang }
|
.filter { it.language == internalLang }
|
||||||
.map { it.toSManga(seriesPath) }
|
.map { it.toSManga() }
|
||||||
|
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||||
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga(seriesPath) } ?: emptyList()
|
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga() } ?: emptyList()
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".zh.komiic.UrlActivity"
|
android:name=".all.hentaicafe.HentaiCafeUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -14,8 +13,8 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="komiic.com"
|
android:host="hentaicafe.xxx"
|
||||||
android:pathPattern="/comic/..*"
|
android:pathPattern="/g/..*"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
|
@ -1,6 +1,6 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hanime1'
|
extName = 'Hentai Cafe'
|
||||||
extClass = '.Hanime1'
|
extClass = '.HentaiCafe'
|
||||||
extVersionCode = 1
|
extVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,167 @@
|
||||||
|
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:"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.pt.yushukemangas
|
package eu.kanade.tachiyomi.extension.all.hentaicafe
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
@ -7,17 +7,22 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class YushukeMangasUrlActivity : Activity() {
|
/**
|
||||||
|
* Springboard that accepts https://hentaicafe.xxx/g/<id> intents
|
||||||
|
* and redirects them to the main Tachiyomi process.
|
||||||
|
*/
|
||||||
|
class HentaiCafeUrlActivity : Activity() {
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
private val tag = javaClass.simpleName
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val id = intent?.data?.getQueryParameter("id")
|
val pathSegments = intent?.data?.pathSegments
|
||||||
if (id != null) {
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
val item = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}$id")
|
putExtra("query", "${HentaiCafe.PREFIX_SEARCH}$item")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,4 @@ package eu.kanade.tachiyomi.extension.all.kemono
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.kemono.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 {
|
ext {
|
||||||
extName = 'Komga'
|
extName = 'Komga'
|
||||||
extClass = '.KomgaFactory'
|
extClass = '.KomgaFactory'
|
||||||
extVersionCode = 58
|
extVersionCode = 57
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -251,10 +251,6 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
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 {
|
override fun getFilterList(): FilterList {
|
||||||
fetchFilterOptions()
|
fetchFilterOptions()
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Miau Scan'
|
extName = 'Miau Scan'
|
||||||
extClass = '.MiauScanFactory'
|
extClass = '.MiauScanFactory'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://zonamiau.com'
|
baseUrl = 'https://lectormiau.com'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -20,7 +20,7 @@ class MiauScanFactory : SourceFactory {
|
||||||
|
|
||||||
open class MiauScan(lang: String) : MangaThemesia(
|
open class MiauScan(lang: String) : MangaThemesia(
|
||||||
name = "Miau Scan",
|
name = "Miau Scan",
|
||||||
baseUrl = "https://zonamiau.com",
|
baseUrl = "https://lectormiau.com",
|
||||||
lang = lang,
|
lang = lang,
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MyReadingManga'
|
extName = 'MyReadingManga'
|
||||||
extClass = '.MyReadingMangaFactory'
|
extClass = '.MyReadingMangaFactory'
|
||||||
extVersionCode = 53
|
extVersionCode = 50
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,7 +147,7 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
||||||
title = cleanTitle(document.select("h1").text())
|
title = cleanTitle(document.select("h1").text())
|
||||||
author = cleanAuthor(document.select("h1").text())
|
author = cleanAuthor(document.select("h1").text())
|
||||||
artist = author
|
artist = author
|
||||||
genre = document.select(".entry-header p a[href*=genre], [href*=tag], span.entry-categories a").joinToString { it.text() }
|
genre = document.select(".entry-header p a[href*=genre]").joinToString { it.text() }
|
||||||
val basicDescription = document.select("h1").text()
|
val basicDescription = document.select("h1").text()
|
||||||
// too troublesome to achieve 100% accuracy assigning scanlator group during chapterListParse
|
// too troublesome to achieve 100% accuracy assigning scanlator group during chapterListParse
|
||||||
val scanlatedBy = document.select(".entry-terms:has(a[href*=group])").firstOrNull()
|
val scanlatedBy = document.select(".entry-terms:has(a[href*=group])").firstOrNull()
|
||||||
|
|
|
@ -23,7 +23,7 @@ private val languageList = listOf(
|
||||||
// Source("", "Finnish"),
|
// Source("", "Finnish"),
|
||||||
// Source("", "Flemish", "flemish-dutch"),
|
// Source("", "Flemish", "flemish-dutch"),
|
||||||
// Source("", "Dutch"),
|
// Source("", "Dutch"),
|
||||||
// Source("fr", "French"),
|
Source("fr", "French"),
|
||||||
Source("de", "German"),
|
Source("de", "German"),
|
||||||
// Source("", "Greek"),
|
// Source("", "Greek"),
|
||||||
// Source("", "Hebrew"),
|
// Source("", "Hebrew"),
|
||||||
|
|
|
@ -9,6 +9,7 @@ class MyRockMangaFactory : SourceFactory {
|
||||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "vi"),
|
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "vi"),
|
||||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "en"),
|
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "en"),
|
||||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "it"),
|
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "it"),
|
||||||
|
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "fr"),
|
||||||
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "es"),
|
OtakuSanctuary("MyRockManga", "https://myrockmanga.com", "es"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Photos18'
|
extName = 'Photos18'
|
||||||
extClass = '.Photos18'
|
extClass = '.Photos18'
|
||||||
extVersionCode = 4
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,8 @@ class Photos18 : HttpSource(), ConfigurableSource {
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
val chapter = SChapter.create().apply {
|
val chapter = SChapter.create().apply {
|
||||||
url = manga.url
|
url = manga.url
|
||||||
name = "Gallery"
|
name = manga.title
|
||||||
chapter_number = 0f
|
chapter_number = -2f
|
||||||
}
|
}
|
||||||
return Observable.just(listOf(chapter))
|
return Observable.just(listOf(chapter))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Pixiv'
|
extName = 'Pixiv'
|
||||||
extClass = '.PixivFactory'
|
extClass = '.PixivFactory'
|
||||||
extVersionCode = 9
|
extVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Pixiv(override val lang: String) : HttpSource() {
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val filters = filters.list as PixivFilters
|
val filters = filters.list as PixivFilters
|
||||||
val hash = Pair(query, filters.toList()).hashCode()
|
val hash = Pair(query, filters).hashCode()
|
||||||
|
|
||||||
if (hash != searchHash || page == 1) {
|
if (hash != searchHash || page == 1) {
|
||||||
searchHash = hash
|
searchHash = hash
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?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>
|
|
@ -0,0 +1,12 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Union Mangas'
|
||||||
|
extClass = '.UnionMangasFactory'
|
||||||
|
extVersionCode = 6
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:cryptoaes'))
|
||||||
|
}
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,210 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
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"),
|
||||||
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
package eu.kanade.tachiyomi.extension.all.unionmangas
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
@ -7,28 +7,30 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class UrlActivity : Activity() {
|
class UnionMangasUrlActivity : Activity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
val id = pathSegments[1]
|
val intent = Intent().apply {
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${Komiic.PREFIX_ID_SEARCH}$id")
|
putExtra("query", slug(pathSegments))
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(intent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("KomiicUrlActivity", e.toString())
|
Log.e("UnionMangasUrlActivity", e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.e("KomiicUrlActivity", "could not parse uri from intent $intent")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun slug(pathSegments: List<String>) =
|
||||||
|
"${UnionMangas.SEARCH_PREFIX}${pathSegments[1]}"
|
||||||
}
|
}
|
|
@ -2,9 +2,8 @@ ext {
|
||||||
extName = 'Area Manga'
|
extName = 'Area Manga'
|
||||||
extClass = '.AreaManga'
|
extClass = '.AreaManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://ar.kenmanga.com'
|
baseUrl = 'https://www.areascans.net'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 0
|
||||||
isNsfw = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import java.util.Locale
|
||||||
|
|
||||||
class AreaManga : MangaThemesia(
|
class AreaManga : MangaThemesia(
|
||||||
"أريا مانجا",
|
"أريا مانجا",
|
||||||
"https://ar.kenmanga.com",
|
"https://www.areascans.net",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'MangaNoon'
|
extName = 'MangaNoon'
|
||||||
extClass = '.MangaNoon'
|
extClass = '.MangaNoon'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://axztu.com'
|
baseUrl = 'https://manjanoon.xyz'
|
||||||
overrideVersionCode = 7
|
overrideVersionCode = 5
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.util.Calendar
|
||||||
|
|
||||||
class MangaNoon : MangaThemesia(
|
class MangaNoon : MangaThemesia(
|
||||||
"مانجا نون",
|
"مانجا نون",
|
||||||
"https://axztu.com",
|
"https://manjanoon.xyz",
|
||||||
"ar",
|
"ar",
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'MangaSwat'
|
extName = 'MangaSwat'
|
||||||
extClass = '.MangaSwat'
|
extClass = '.MangaSwat'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://swatscans.com'
|
baseUrl = 'https://healteer.com'
|
||||||
overrideVersionCode = 24
|
overrideVersionCode = 23
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -22,7 +22,7 @@ import java.util.Locale
|
||||||
class MangaSwat :
|
class MangaSwat :
|
||||||
MangaThemesia(
|
MangaThemesia(
|
||||||
"MangaSwat",
|
"MangaSwat",
|
||||||
"https://swatscans.com",
|
"https://healteer.com",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,12 +2,4 @@ package eu.kanade.tachiyomi.extension.ar.scans4u
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
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(قادم)))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Toomics.Top'
|
||||||
|
extClass = '.ToomicsTop'
|
||||||
|
themePkg = 'hotcomics'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,44 @@
|
||||||
|
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'
|
extClass = '.AdultWebtoon'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://adultwebtoon.com'
|
baseUrl = 'https://adultwebtoon.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.extension.en.adultwebtoon
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
||||||
override val mangaSubString = "adult-webtoon"
|
override val mangaSubString = "adult-webtoon"
|
||||||
|
@ -13,8 +15,15 @@ class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.next"
|
override fun popularMangaNextPageSelector() = "a.next"
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = "li.movie-item > a"
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
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 oldXhrChaptersRequest(mangaId: String): Request {
|
override fun oldXhrChaptersRequest(mangaId: String): Request {
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Arven Scans'
|
extName = 'Arven Scans'
|
||||||
extClass = '.ArvenComics'
|
extClass = '.ArvenComics'
|
||||||
themePkg = 'keyoapp'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://arvencomics.com'
|
baseUrl = 'https://arvencomics.com'
|
||||||
overrideVersionCode = 24
|
overrideVersionCode = 0
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvencomics
|
package eu.kanade.tachiyomi.extension.en.arvencomics
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.keyoapp.Keyoapp
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
|
||||||
class ArvenComics : Keyoapp(
|
class ArvenComics : MangaThemesia(
|
||||||
"Arven Scans",
|
"Arven Scans",
|
||||||
"https://arvencomics.com",
|
"https://arvencomics.com",
|
||||||
"en",
|
"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 {
|
ext {
|
||||||
extName = 'Asura Scans'
|
extName = 'Asura Scans'
|
||||||
extClass = '.AsuraScans'
|
extClass = '.AsuraScans'
|
||||||
extVersionCode = 41
|
extVersionCode = 39
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -57,9 +57,6 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
if (contains("pref_permanent_manga_url_2_en")) {
|
if (contains("pref_permanent_manga_url_2_en")) {
|
||||||
edit().remove("pref_permanent_manga_url_2_en").apply()
|
edit().remove("pref_permanent_manga_url_2_en").apply()
|
||||||
}
|
}
|
||||||
if (contains("pref_slug_map")) {
|
|
||||||
edit().remove("pref_slug_map").apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,12 +192,10 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
if (preferences.dynamicUrl()) {
|
if (preferences.dynamicUrl()) {
|
||||||
val url = response.request.url.toString()
|
val url = response.request.url.toString()
|
||||||
val newSlug = url.substringAfter("/series/", "").substringBefore("/")
|
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
||||||
if (newSlug.isNotEmpty()) {
|
|
||||||
val absSlug = newSlug.substringBeforeLast("-")
|
val absSlug = newSlug.substringBeforeLast("-")
|
||||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return super.mangaDetailsParse(response)
|
return super.mangaDetailsParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,12 +225,10 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
if (preferences.dynamicUrl()) {
|
if (preferences.dynamicUrl()) {
|
||||||
val url = response.request.url.toString()
|
val url = response.request.url.toString()
|
||||||
val newSlug = url.substringAfter("/series/", "").substringBefore("/")
|
val newSlug = url.substringAfter("/series/").substringBefore("/")
|
||||||
if (newSlug.isNotEmpty()) {
|
|
||||||
val absSlug = newSlug.substringBeforeLast("-")
|
val absSlug = newSlug.substringBeforeLast("-")
|
||||||
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
preferences.slugMap = preferences.slugMap.apply { put(absSlug, newSlug) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return super.chapterListParse(response)
|
return super.chapterListParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,9 +238,9 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href").toPermSlugIfNeeded())
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href").toPermSlugIfNeeded())
|
||||||
name = element.selectFirst("h3")!!.text()
|
name = element.selectFirst("h3:eq(0)")!!.text()
|
||||||
date_upload = try {
|
date_upload = try {
|
||||||
val text = element.selectFirst("h3 + h3")!!.ownText()
|
val text = element.selectFirst("h3:eq(1)")!!.ownText()
|
||||||
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
|
val cleanText = text.replace(CLEAN_DATE_REGEX, "$1")
|
||||||
dateFormat.parse(cleanText)?.time ?: 0
|
dateFormat.parse(cleanText)?.time ?: 0
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
@ -315,7 +308,7 @@ class AsuraScans : ParsedHttpSource(), ConfigurableSource {
|
||||||
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
|
private val CLEAN_DATE_REGEX = """(\d+)(st|nd|rd|th)""".toRegex()
|
||||||
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
|
private val OLD_FORMAT_MANGA_REGEX = """^/manga/(\d+-)?([^/]+)/?$""".toRegex()
|
||||||
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
|
private val OLD_FORMAT_CHAPTER_REGEX = """^/(\d+-)?[^/]*-chapter-\d+(-\d+)*/?$""".toRegex()
|
||||||
private const val PREF_SLUG_MAP = "pref_slug_map_2"
|
private const val PREF_SLUG_MAP = "pref_slug_map"
|
||||||
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
|
private const val PREF_DYNAMIC_URL = "pref_dynamic_url"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'BatCave'
|
|
||||||
extClass = '.BatCave'
|
|
||||||
extVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 7.0 KiB |
|
@ -1,239 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
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>,
|
|
||||||
)
|
|
|
@ -1,113 +0,0 @@
|
||||||
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",
|
|
||||||
)
|
|