Compare commits

..

No commits in common. "cb2794830738e740114e15a59b50655413e0f059" and "e0bb39f99adb84553c28dce5f190fbeb60fb80b9" have entirely different histories.

932 changed files with 3608 additions and 5903 deletions

View File

@ -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

View File

@ -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

View File

@ -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" }

Binary file not shown.

View File

@ -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

7
gradlew generated vendored
View File

@ -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

2
gradlew.bat generated vendored
View File

@ -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 ##########################################################################

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 3 baseVersionCode = 2

View File

@ -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())

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 1

View File

@ -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"))

View File

@ -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"))

View File

@ -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())
} }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 14 baseVersionCode = 12

View File

@ -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 { private fun fetchCreatorsPage(
var sort = sortBy page: Int,
val typeIncluded: MutableList<String> = mutableListOf() block: (ArrayList<KemonoCreatorDto>) -> List<KemonoCreatorDto>,
val typeExcluded: MutableList<String> = mutableListOf() ): MangasPage {
var fav: Boolean? = null val imgCdnUrl = this.imgCdnUrl
filters?.forEach { filter -> val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
when (filter) { val allCreators = block(response.parseAs())
is SortFilter -> { val count = allCreators.size
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc" val fromIndex = (page - 1) * NEW_PAGE_SIZE
} val toIndex = min(count, fromIndex + NEW_PAGE_SIZE)
is TypeFilter -> { val creators = allCreators.subList(fromIndex, toIndex)
filter.state.filter { state -> state.isIncluded() }.forEach { tri -> .map { it.toSManga(imgCdnUrl) }
typeIncluded.add(tri.value) .filterUnsupported()
} return MangasPage(creators, toIndex < count)
}
filter.state.filter { state -> state.isExcluded() }.forEach { tri -> private fun cacheCreators() {
typeExcluded.add(tri.value) val callback = object : Callback {
} override fun onResponse(call: Call, response: Response) =
response.body.source().run {
readAll(blackholeSink())
close()
} }
is FavouritesFilter -> {
fav = when (filter.state[0].state) { override fun onFailure(call: Call, e: IOException) = Unit
0 -> null
1 -> true
else -> false
}
}
else -> {}
}
} }
client.newCall(GET("$baseUrl/$apiPath/creators", headers)).enqueue(callback)
var mangas = mangasCache
if (page == 1) {
var favourites: List<KemonoFavouritesDto> = emptyList()
if (fav != null) {
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
if (favores.code == 401) throw Exception("You are not Logged In")
favourites = favores.parseAs<List<KemonoFavouritesDto>>().filterNot { it.service.lowercase() == "discord" }
}
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
mangas = allCreators.filter {
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
val regularSearch = it.name.contains(title, true)
val isFavourited = when (fav) {
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
false -> favourites.none { f -> f.id == it.id }
else -> true
}
includeType && !excludeType && isFavourited &&
regularSearch
}.also { mangasCache = mangas }
}
val sorted = when (sort.first) {
"pop" -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.favorited }
} else {
mangas.sortedBy { it.favorited }
}
}
"tit" -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.name }
} else {
mangas.sortedBy { it.name }
}
}
"new" -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.id }
} else {
mangas.sortedBy { it.id }
}
}
"fav" -> {
if (fav != true) throw Exception("Please check 'Favourites Only' Filter")
if (sort.second == "desc") {
mangas.sortedByDescending { it.fav }
} else {
mangas.sortedBy { it.fav }
}
}
else -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.updatedDate }
} else {
mangas.sortedBy { it.updatedDate }
}
}
}
val maxIndex = mangas.size
val fromIndex = (page - 1) * PAGE_CREATORS_LIMIT
val toIndex = min(maxIndex, fromIndex + PAGE_CREATORS_LIMIT)
val final = sorted.subList(fromIndex, toIndex).map { it.toSManga(imgCdnUrl) }
return MangasPage(final, toIndex != maxIndex)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() 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"

View File

@ -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()

View File

@ -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.

View File

@ -2,8 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 8 baseVersionCode = 7
dependencies {
api(project(":lib:i18n"))
}

View File

@ -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
}
} }

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -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
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 32 baseVersionCode = 31

View File

@ -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(

View File

@ -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> {

View File

@ -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"
} }
} }

View File

@ -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) {

View File

@ -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

View File

@ -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) }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Bato.to' extName = 'Bato.to'
extClass = '.BatoToFactory' extClass = '.BatoToFactory'
extVersionCode = 41 extVersionCode = 37
isNsfw = true isNsfw = true
} }

View File

@ -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",

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comick' extName = 'Comick'
extClass = '.ComickFactory' extClass = '.ComickFactory'
extVersionCode = 48 extVersionCode = 47
isNsfw = true isNsfw = true
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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",
)
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'CosplayTele' extName = 'CosplayTele'
extClass = '.CosplayTele' extClass = '.CosplayTele'
extVersionCode = 3 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -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)

View File

@ -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() {

View File

@ -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)
} }

View File

@ -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>

View File

@ -1,6 +1,6 @@
ext { ext {
extName = 'Hanime1' extName = 'Hentai Cafe'
extClass = '.Hanime1' extClass = '.HentaiCafe'
extVersionCode = 1 extVersionCode = 1
isNsfw = true isNsfw = true
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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:"
}
}

View File

@ -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)
} }

View File

@ -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",
)
}

View File

@ -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"

View File

@ -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()

View File

@ -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"

View File

@ -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")),
) { ) {

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MyReadingManga' extName = 'MyReadingManga'
extClass = '.MyReadingMangaFactory' extClass = '.MyReadingMangaFactory'
extVersionCode = 53 extVersionCode = 50
isNsfw = true isNsfw = true
} }

View File

@ -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()

View File

@ -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"),

View File

@ -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"),
) )
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Photos18' extName = 'Photos18'
extClass = '.Photos18' extClass = '.Photos18'
extVersionCode = 4 extVersionCode = 3
isNsfw = true isNsfw = true
} }

View File

@ -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))
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Pixiv' extName = 'Pixiv'
extClass = '.PixivFactory' extClass = '.PixivFactory'
extVersionCode = 9 extVersionCode = 8
isNsfw = true isNsfw = true
} }

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,12 @@
ext {
extName = 'Union Mangas'
extClass = '.UnionMangasFactory'
extVersionCode = 6
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:cryptoaes'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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"),
)

View File

@ -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]}"
} }

View File

@ -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"

View File

@ -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")),
) )

View File

@ -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
} }

View File

@ -7,7 +7,7 @@ import java.util.Calendar
class MangaNoon : MangaThemesia( class MangaNoon : MangaThemesia(
"مانجا نون", "مانجا نون",
"https://axztu.com", "https://manjanoon.xyz",
"ar", "ar",
) { ) {

View File

@ -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"

View File

@ -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")),
), ),

View File

@ -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(قادم)))"
}
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'Toomics.Top'
extClass = '.ToomicsTop'
themePkg = 'hotcomics'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"),
)
}

View File

@ -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
} }

View File

@ -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()

View File

@ -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
} }

View File

@ -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"
}

View File

@ -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"

View File

@ -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,11 +192,9 @@ 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,11 +225,9 @@ 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"
} }
} }

View File

@ -1,7 +0,0 @@
ext {
extName = 'BatCave'
extClass = '.BatCave'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -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)
}
}

View File

@ -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>,
)

View File

@ -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",
)

Some files were not shown because too many files have changed in this diff Show More