Compare commits
28 Commits
555051eba4
...
5292a9ff0a
Author | SHA1 | Date |
---|---|---|
AwkwardPeak7 | 5292a9ff0a | |
bapeey | 0d04d70929 | |
devil6venom | 8a8e4d2a8d | |
bapeey | b1b8833e97 | |
bapeey | 696a40e725 | |
bapeey | 8be756b8bc | |
bapeey | a3039453b0 | |
Ashborn | c445ca0eb4 | |
Cuong M. Tran | 66dd223155 | |
AwkwardPeak7 | 33bfee0f2a | |
AwkwardPeak7 | 1d7c252a48 | |
bapeey | dcf9230a21 | |
AwkwardPeak7 | 2f4f7001ea | |
GoldenRover | e2e0d9b034 | |
AwkwardPeak7 | a2e3223685 | |
Chopper | 8ec772ebbe | |
Karuto | 085efa9fa4 | |
KirinRaikage | f5ec446f74 | |
AwkwardPeak7 | 9a7187bf36 | |
AwkwardPeak7 | 20db7af324 | |
AwkwardPeak7 | 635f732acf | |
AwkwardPeak7 | 5ff2906e28 | |
AwkwardPeak7 | 1abb2f0fa6 | |
AwkwardPeak7 | 1cd1935eeb | |
AwkwardPeak7 | 757b067214 | |
Luqman | f42f301af4 | |
bapeey | 3a76ddaf34 | |
mohamedotaku | 5a1fdc542c |
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
package eu.kanade.tachiyomi.multisrc.gmanga
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
@ -14,7 +14,7 @@ fun decrypt(responseData: String): String {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.hexStringToByteArray(): ByteArray {
|
private fun String.hexStringToByteArray(): ByteArray {
|
||||||
val len = this.length
|
val len = length
|
||||||
val data = ByteArray(len / 2)
|
val data = ByteArray(len / 2)
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
|
@ -30,8 +30,8 @@ private fun String.hexStringToByteArray(): ByteArray {
|
||||||
private fun String.sha256(): String {
|
private fun String.sha256(): String {
|
||||||
return MessageDigest
|
return MessageDigest
|
||||||
.getInstance("SHA-256")
|
.getInstance("SHA-256")
|
||||||
.digest(this.toByteArray())
|
.digest(toByteArray())
|
||||||
.fold("", { str, it -> str + "%02x".format(it) })
|
.fold("") { str, it -> str + "%02x".format(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
||||||
|
@ -40,6 +40,6 @@ private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
||||||
val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
|
val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
|
||||||
c.init(Cipher.DECRYPT_MODE, sk, iv)
|
c.init(Cipher.DECRYPT_MODE, sk, iv)
|
||||||
|
|
||||||
val byteStr = Base64.decode(this.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
val byteStr = Base64.decode(toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
||||||
return String(c.doFinal(byteStr))
|
return String(c.doFinal(byteStr))
|
||||||
}
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.gmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EncryptedResponse(val data: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDataAction<T>(val mangaDataAction: T)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class LatestChaptersDto(
|
||||||
|
val releases: List<LatestReleaseDto>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class LatestReleaseDto(
|
||||||
|
val manga: BrowseManga,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchMangaDto(
|
||||||
|
val mangas: List<BrowseManga>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BrowseManga(
|
||||||
|
private val id: Int,
|
||||||
|
private val title: String,
|
||||||
|
private val cover: String,
|
||||||
|
) {
|
||||||
|
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
||||||
|
url = "/mangas/$id"
|
||||||
|
title = this@BrowseManga.title
|
||||||
|
thumbnail_url = createThumbnail(id.toString(), cover)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class FiltersDto(
|
||||||
|
val categoryTypes: List<FiltersDto>? = null,
|
||||||
|
val categories: List<FilterDto>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class FilterDto(
|
||||||
|
val name: String,
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDetailsDto(
|
||||||
|
val mangaData: Manga,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Manga(
|
||||||
|
private val id: Int,
|
||||||
|
private val cover: String,
|
||||||
|
private val title: String,
|
||||||
|
private val summary: String? = null,
|
||||||
|
private val artists: List<NameDto>,
|
||||||
|
private val authors: List<NameDto>,
|
||||||
|
@SerialName("story_status") private val status: Int,
|
||||||
|
private val type: TypeDto,
|
||||||
|
private val categories: List<NameDto>,
|
||||||
|
@SerialName("translation_status") private val tlStatus: Int,
|
||||||
|
private val synonyms: String? = null,
|
||||||
|
@SerialName("arabic_title") private val arTitle: String? = null,
|
||||||
|
@SerialName("japanese") private val jpTitle: String? = null,
|
||||||
|
@SerialName("english") private val enTitle: String? = null,
|
||||||
|
) {
|
||||||
|
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
||||||
|
title = this@Manga.title
|
||||||
|
thumbnail_url = createThumbnail(id.toString(), cover)
|
||||||
|
artist = artists.joinToString { it.name }
|
||||||
|
author = authors.joinToString { it.name }
|
||||||
|
status = when (this@Manga.status) {
|
||||||
|
2 -> SManga.ONGOING
|
||||||
|
3 -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
genre = buildList {
|
||||||
|
add(type.title)
|
||||||
|
add(type.name)
|
||||||
|
categories.forEach { add(it.name) }
|
||||||
|
}.joinToString()
|
||||||
|
description = buildString {
|
||||||
|
summary.orEmpty()
|
||||||
|
.ifEmpty { "لم يتم اضافة قصة بعد" }
|
||||||
|
.also { append(it) }
|
||||||
|
|
||||||
|
when (tlStatus) {
|
||||||
|
0 -> "منتهية"
|
||||||
|
1 -> "مستمرة"
|
||||||
|
2 -> "متوقفة"
|
||||||
|
else -> "مجهول"
|
||||||
|
}.also {
|
||||||
|
append("\n\n")
|
||||||
|
append("حالة الترجمة")
|
||||||
|
append(":\n• ")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val titles = listOfNotNull(synonyms, arTitle, jpTitle, enTitle)
|
||||||
|
if (titles.isNotEmpty()) {
|
||||||
|
append("\n\n")
|
||||||
|
append("مسميّات أخرى")
|
||||||
|
append(":\n• ")
|
||||||
|
append(titles.joinToString("\n• "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class NameDto(val name: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class TypeDto(
|
||||||
|
val name: String,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderDto(
|
||||||
|
val readerDataAction: ReaderData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderData(
|
||||||
|
val readerData: ReaderChapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderChapter(
|
||||||
|
val release: ReaderPages,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderPages(
|
||||||
|
@SerialName("webp_pages") val webpPages: List<String>,
|
||||||
|
val pages: List<String>,
|
||||||
|
@SerialName("storage_key") val key: String,
|
||||||
|
)
|
|
@ -0,0 +1,94 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.gmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class TagFilterData(
|
||||||
|
private val id: String,
|
||||||
|
private val name: String,
|
||||||
|
private val state: Int = Filter.TriState.STATE_IGNORE,
|
||||||
|
) {
|
||||||
|
fun toTagFilter() = TagFilter(id, name, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagFilter(
|
||||||
|
val id: String,
|
||||||
|
name: String,
|
||||||
|
state: Int = STATE_IGNORE,
|
||||||
|
) : Filter.TriState(name, state)
|
||||||
|
|
||||||
|
abstract class ValidatingTextFilter(name: String) : Filter.Text(name) {
|
||||||
|
abstract fun isValid(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DATE_FITLER_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH).apply {
|
||||||
|
isLenient = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SimpleDateFormat.isValid(date: String): Boolean {
|
||||||
|
return try {
|
||||||
|
parse(date)
|
||||||
|
true
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateFilter(val id: String, name: String) : ValidatingTextFilter("(yyyy/MM/dd) $name)") {
|
||||||
|
override fun isValid(): Boolean = DATE_FITLER_FORMAT.isValid(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntFilter(val id: String, name: String) : ValidatingTextFilter(name) {
|
||||||
|
override fun isValid(): Boolean = state.toIntOrNull() != null
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaTypeFilter(types: List<TagFilterData>) : Filter.Group<TagFilter>(
|
||||||
|
"الأصل",
|
||||||
|
types.map { it.toTagFilter() },
|
||||||
|
)
|
||||||
|
|
||||||
|
class OneShotFilter : Filter.Group<TagFilter>(
|
||||||
|
"ونشوت؟",
|
||||||
|
listOf(
|
||||||
|
TagFilter("oneshot", "نعم", TriState.STATE_EXCLUDE),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StoryStatusFilter(status: List<TagFilterData>) : Filter.Group<TagFilter>(
|
||||||
|
"حالة القصة",
|
||||||
|
status.map { it.toTagFilter() },
|
||||||
|
)
|
||||||
|
|
||||||
|
class TranslationStatusFilter(tlStatus: List<TagFilterData>) : Filter.Group<TagFilter>(
|
||||||
|
"حالة الترجمة",
|
||||||
|
tlStatus.map { it.toTagFilter() },
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterCountFilter : Filter.Group<IntFilter>(
|
||||||
|
"عدد الفصول",
|
||||||
|
listOf(
|
||||||
|
IntFilter("min", "على الأقل"),
|
||||||
|
IntFilter("max", "على الأكثر"),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val min get() = state.first { it.id == "min" }
|
||||||
|
val max get() = state.first { it.id == "max" }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateRangeFilter : Filter.Group<DateFilter>(
|
||||||
|
"تاريخ النشر",
|
||||||
|
listOf(
|
||||||
|
DateFilter("start", "تاريخ النشر"),
|
||||||
|
DateFilter("end", "تاريخ الإنتهاء"),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val start get() = state.first { it.id == "start" }
|
||||||
|
val end get() = state.first { it.id == "end" }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryFilter(categories: List<TagFilterData>) : Filter.Group<TagFilter>(
|
||||||
|
"التصنيفات",
|
||||||
|
categories.map { it.toTagFilter() },
|
||||||
|
)
|
|
@ -0,0 +1,297 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.gmanga
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
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.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class Gmanga(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String,
|
||||||
|
protected val cdnUrl: String = baseUrl,
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
protected val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
||||||
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/api/releases?page=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val releases = response.parseAs<LatestChaptersDto>().releases
|
||||||
|
|
||||||
|
val entries = releases.map { it.manga.toSManga(::createThumbnail) }
|
||||||
|
.distinctBy { it.url }
|
||||||
|
|
||||||
|
return MangasPage(
|
||||||
|
entries,
|
||||||
|
hasNextPage = (releases.size >= 30),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
|
||||||
|
val mangaTypeFilter = filterList.findInstance<MangaTypeFilter>()!!
|
||||||
|
val oneShotFilter = filterList.findInstance<OneShotFilter>()!!
|
||||||
|
val storyStatusFilter = filterList.findInstance<StoryStatusFilter>()!!
|
||||||
|
val translationStatusFilter = filterList.findInstance<TranslationStatusFilter>()!!
|
||||||
|
val chapterCountFilter = filterList.findInstance<ChapterCountFilter>()!!
|
||||||
|
val dateRangeFilter = filterList.findInstance<DateRangeFilter>()!!
|
||||||
|
val categoryFilter = filterList.findInstance<CategoryFilter>() ?: CategoryFilter(emptyList())
|
||||||
|
|
||||||
|
val body = SearchPayload(
|
||||||
|
oneshot = OneShot(
|
||||||
|
value = oneShotFilter.state.first().run {
|
||||||
|
when {
|
||||||
|
isIncluded() -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title = query,
|
||||||
|
page = page,
|
||||||
|
mangaTypes = IncludeExclude(
|
||||||
|
include = mangaTypeFilter.state.filter { it.isIncluded() }.map { it.id },
|
||||||
|
exclude = mangaTypeFilter.state.filter { it.isExcluded() }.map { it.id },
|
||||||
|
),
|
||||||
|
storyStatus = IncludeExclude(
|
||||||
|
include = storyStatusFilter.state.filter { it.isIncluded() }.map { it.id },
|
||||||
|
exclude = storyStatusFilter.state.filter { it.isExcluded() }.map { it.id },
|
||||||
|
),
|
||||||
|
tlStatus = IncludeExclude(
|
||||||
|
include = translationStatusFilter.state.filter { it.isIncluded() }.map { it.id },
|
||||||
|
exclude = translationStatusFilter.state.filter { it.isExcluded() }.map { it.id },
|
||||||
|
),
|
||||||
|
categories = IncludeExclude(
|
||||||
|
// always include null, maybe to avoid shifting index in the backend
|
||||||
|
include = listOf(null) + categoryFilter.state.filter { it.isIncluded() }.map { it.id },
|
||||||
|
exclude = categoryFilter.state.filter { it.isExcluded() }.map { it.id },
|
||||||
|
),
|
||||||
|
chapters = MinMax(
|
||||||
|
min = chapterCountFilter.min.run {
|
||||||
|
when {
|
||||||
|
state == "" -> ""
|
||||||
|
isValid() -> state
|
||||||
|
else -> throw Exception("الحد الأدنى لعدد الفصول غير صالح")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
max = chapterCountFilter.max.run {
|
||||||
|
when {
|
||||||
|
state == "" -> ""
|
||||||
|
isValid() -> state
|
||||||
|
else -> throw Exception("الحد الأقصى لعدد الفصول غير صالح")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dates = StartEnd(
|
||||||
|
start = dateRangeFilter.start.run {
|
||||||
|
when {
|
||||||
|
state == "" -> ""
|
||||||
|
isValid() -> state
|
||||||
|
else -> throw Exception("تاريخ بداية غير صالح")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end = dateRangeFilter.end.run {
|
||||||
|
when {
|
||||||
|
state == "" -> ""
|
||||||
|
isValid() -> state
|
||||||
|
else -> throw Exception("تاريخ نهاية غير صالح")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).let(json::encodeToString).toRequestBody(MEDIA_TYPE)
|
||||||
|
|
||||||
|
return POST("$baseUrl/api/mangas/search", headers, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var categories: List<TagFilterData> = emptyList()
|
||||||
|
private var filtersState = FilterState.Unfetched
|
||||||
|
private var filterAttempts = 0
|
||||||
|
|
||||||
|
private enum class FilterState {
|
||||||
|
Fetching, Fetched, Unfetched
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchFilters() {
|
||||||
|
if (filtersState == FilterState.Unfetched && filterAttempts < 3) {
|
||||||
|
filtersState = FilterState.Fetching
|
||||||
|
filterAttempts++
|
||||||
|
|
||||||
|
try {
|
||||||
|
categories = client.newCall(GET("$baseUrl/mangas/", headers))
|
||||||
|
.await()
|
||||||
|
.asJsoup()
|
||||||
|
.select(".js-react-on-rails-component").html()
|
||||||
|
.parseAs<FiltersDto>()
|
||||||
|
.run {
|
||||||
|
categories ?: categoryTypes!!.flatMap { it.categories!! }
|
||||||
|
}
|
||||||
|
.map { TagFilterData(it.id.toString(), it.name) }
|
||||||
|
|
||||||
|
filtersState = FilterState.Fetched
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(name, e.stackTraceToString())
|
||||||
|
filtersState = FilterState.Unfetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getTypesFilter() = listOf(
|
||||||
|
TagFilterData("1", "يابانية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("2", "كورية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("3", "صينية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("4", "عربية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("5", "كوميك", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("6", "هواة", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("7", "إندونيسية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("8", "روسية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open fun getStatusFilter() = listOf(
|
||||||
|
TagFilterData("2", "مستمرة"),
|
||||||
|
TagFilterData("3", "منتهية"),
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open fun getTranslationFilter() = listOf(
|
||||||
|
TagFilterData("0", "منتهية"),
|
||||||
|
TagFilterData("1", "مستمرة"),
|
||||||
|
TagFilterData("2", "متوقفة"),
|
||||||
|
TagFilterData("3", "غير مترجمة", Filter.TriState.STATE_EXCLUDE),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { fetchFilters() }
|
||||||
|
|
||||||
|
val filters = mutableListOf<Filter<*>>(
|
||||||
|
MangaTypeFilter(getTypesFilter()),
|
||||||
|
OneShotFilter(),
|
||||||
|
StoryStatusFilter(getStatusFilter()),
|
||||||
|
TranslationStatusFilter(getTranslationFilter()),
|
||||||
|
ChapterCountFilter(),
|
||||||
|
DateRangeFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
filters += if (filtersState == FilterState.Fetched) {
|
||||||
|
listOf(
|
||||||
|
CategoryFilter(categories),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("اضغط على\"إعادة تعيين\"لمحاولة تحميل التصنيفات"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val data = response.decryptAs<SearchMangaDto>()
|
||||||
|
return MangasPage(
|
||||||
|
data.mangas.map { it.toSManga(::createThumbnail) },
|
||||||
|
hasNextPage = data.mangas.size == 50,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
return response.asJsoup()
|
||||||
|
.select(".js-react-on-rails-component").html()
|
||||||
|
.parseAs<MangaDataAction<MangaDetailsDto>>()
|
||||||
|
.mangaDataAction.mangaData
|
||||||
|
.toSManga(::createThumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun chaptersRequest(manga: SManga): Request
|
||||||
|
abstract fun chaptersParse(response: Response): List<SChapter>
|
||||||
|
|
||||||
|
final override fun chapterListRequest(manga: SManga) = chaptersRequest(manga)
|
||||||
|
final override fun chapterListParse(response: Response) = chaptersParse(response).sortChapters()
|
||||||
|
|
||||||
|
private fun List<SChapter>.sortChapters() =
|
||||||
|
sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ -it.chapter_number },
|
||||||
|
{ -it.date_upload },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val data = response.asJsoup()
|
||||||
|
.select(".js-react-on-rails-component").html()
|
||||||
|
.parseAs<ReaderDto>()
|
||||||
|
.readerDataAction.readerData.release
|
||||||
|
|
||||||
|
val hasWebP = data.webpPages.isNotEmpty()
|
||||||
|
|
||||||
|
val (pages, directory) = when {
|
||||||
|
hasWebP -> data.webpPages to "hq_webp"
|
||||||
|
else -> data.pages to "hq"
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages.sortedWith(pageSort).mapIndexed { index, pageUri ->
|
||||||
|
Page(
|
||||||
|
index = index,
|
||||||
|
imageUrl = "$cdnUrl/uploads/releases/${data.key}/$directory/$pageUri",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pageSort =
|
||||||
|
compareBy<String>({ parseNumber(0, it) ?: Double.MAX_VALUE }, { parseNumber(1, it) }, { parseNumber(2, it) })
|
||||||
|
|
||||||
|
private fun parseNumber(index: Int, string: String): Double? =
|
||||||
|
Regex("\\d+").findAll(string).map { it.value }.toList().getOrNull(index)?.toDoubleOrNull()
|
||||||
|
|
||||||
|
protected inline fun <reified T> Response.decryptAs(): T =
|
||||||
|
decrypt(parseAs<EncryptedResponse>().data).parseAs()
|
||||||
|
|
||||||
|
protected inline fun <reified T> Response.parseAs(): T = body.string().parseAs()
|
||||||
|
|
||||||
|
protected inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||||
|
|
||||||
|
protected inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
protected open fun createThumbnail(mangaId: String, cover: String): String {
|
||||||
|
val thumbnail = "large_${cover.substringBeforeLast(".")}.webp"
|
||||||
|
|
||||||
|
return "$cdnUrl/uploads/manga/cover/$mangaId/$thumbnail"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.gmanga
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchPayload(
|
||||||
|
private val oneshot: OneShot,
|
||||||
|
private val title: String,
|
||||||
|
private val page: Int,
|
||||||
|
@SerialName("manga_types") private val mangaTypes: IncludeExclude,
|
||||||
|
@SerialName("story_status") private val storyStatus: IncludeExclude,
|
||||||
|
@SerialName("translation_status") val tlStatus: IncludeExclude,
|
||||||
|
private val categories: IncludeExclude,
|
||||||
|
private val chapters: MinMax,
|
||||||
|
private val dates: StartEnd,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class OneShot(
|
||||||
|
private val value: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class IncludeExclude(
|
||||||
|
private val include: List<String?>,
|
||||||
|
private val exclude: List<String?>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MinMax(
|
||||||
|
private val min: String,
|
||||||
|
private val max: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class StartEnd(
|
||||||
|
private val start: String,
|
||||||
|
private val end: String,
|
||||||
|
)
|
|
@ -237,7 +237,7 @@ abstract class Madara(
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
||||||
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}"
|
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
|
||||||
return client.newCall(GET("$baseUrl$mangaUrl", headers))
|
return client.newCall(GET("$baseUrl$mangaUrl", headers))
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val manga = mangaDetailsParse(response).apply {
|
val manga = mangaDetailsParse(response).apply {
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 29
|
baseVersionCode = 30
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|
|
@ -272,30 +272,31 @@ abstract class MangaThemesia(
|
||||||
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
|
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun String?.parseStatus(): Int {
|
open fun String?.parseStatus(): Int = when {
|
||||||
if (this == null) return SManga.UNKNOWN
|
this == null -> SManga.UNKNOWN
|
||||||
|
|
||||||
return when (this.lowercase().trim()) {
|
listOf(
|
||||||
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
|
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours", "en cours de publication",
|
||||||
"en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing",
|
"đang tiến hành", "em lançamento", "онгоінг", "publishing", "devam ediyor", "em andamento",
|
||||||
"devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision",
|
"in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo",
|
||||||
"en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando", "连载中", "devam etmekte", "連載中",
|
"emision", "en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando",
|
||||||
-> SManga.ONGOING
|
"连载中", "devam etmekte", "連載中",
|
||||||
|
).any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
|
||||||
|
|
||||||
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành", "hoàn thành",
|
listOf(
|
||||||
"مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot", "bitti", "tamat", "completado", "concluído", "完結",
|
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành",
|
||||||
"concluido", "已完结", "bitmiş",
|
"hoàn thành", "مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot",
|
||||||
-> SManga.COMPLETED
|
"bitti", "tamat", "completado", "concluído", "完結", "concluido", "已完结", "bitmiş",
|
||||||
|
).any { this.contains(it, ignoreCase = true) } -> SManga.COMPLETED
|
||||||
|
|
||||||
"canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné",
|
listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné")
|
||||||
-> SManga.CANCELLED
|
.any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
||||||
|
|
||||||
"hiatus", "on hold", "pausado", "en espera", "en pause", "en attente",
|
listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente")
|
||||||
-> SManga.ON_HIATUS
|
.any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS
|
||||||
|
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter list
|
// Chapter list
|
||||||
override fun chapterListSelector() = "div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)"
|
override fun chapterListSelector() = "div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)"
|
||||||
|
|
|
@ -391,6 +391,7 @@ abstract class ZeistManga(
|
||||||
protected open val statusOnGoingList = listOf(
|
protected open val statusOnGoingList = listOf(
|
||||||
"ongoing",
|
"ongoing",
|
||||||
"en curso",
|
"en curso",
|
||||||
|
"en emisión",
|
||||||
"ativo",
|
"ativo",
|
||||||
"lançando",
|
"lançando",
|
||||||
"مستمر",
|
"مستمر",
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Dilar'
|
||||||
|
extClass = '.Dilar'
|
||||||
|
themePkg = 'gmanga'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 9.0 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.dilar
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class Dilar : Gmanga(
|
||||||
|
"Dilar",
|
||||||
|
"https://dilar.tube",
|
||||||
|
"ar",
|
||||||
|
) {
|
||||||
|
override fun chaptersRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.substringAfterLast("/")
|
||||||
|
return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chaptersParse(response: Response): List<SChapter> {
|
||||||
|
val releases = response.parseAs<ChapterListDto>().releases
|
||||||
|
.filterNot { it.isMonetized }
|
||||||
|
|
||||||
|
return releases.map { it.toSChapter() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.dilar
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListDto(
|
||||||
|
val releases: List<ChapterRelease>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterRelease(
|
||||||
|
private val id: Int,
|
||||||
|
private val chapter: JsonPrimitive,
|
||||||
|
private val title: String,
|
||||||
|
@SerialName("team_name") private val teamName: String,
|
||||||
|
@SerialName("time_stamp") private val timestamp: Long,
|
||||||
|
|
||||||
|
@SerialName("has_rev_link") private val hasRevLink: Boolean,
|
||||||
|
@SerialName("support_link") private val supportLink: String,
|
||||||
|
) {
|
||||||
|
val isMonetized get() = hasRevLink && supportLink.isNotEmpty()
|
||||||
|
|
||||||
|
fun toSChapter() = SChapter.create().apply {
|
||||||
|
url = "/r/$id"
|
||||||
|
chapter_number = chapter.float
|
||||||
|
date_upload = timestamp * 1000
|
||||||
|
scanlator = teamName
|
||||||
|
|
||||||
|
val chapterName = title.let { if (it.trim() != "") " - $it" else "" }
|
||||||
|
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'GMANGA'
|
extName = 'GMANGA'
|
||||||
extClass = '.Gmanga'
|
extClass = '.Gmanga'
|
||||||
extVersionCode = 13
|
themePkg = 'gmanga'
|
||||||
|
overrideVersionCode = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListResponse(
|
||||||
|
val releases: List<ChapterRelease>,
|
||||||
|
val chapterizations: List<Chapterization>,
|
||||||
|
val teams: List<Team>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterRelease(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("chapterization_id") val chapId: Int,
|
||||||
|
@SerialName("team_id") val teamId: Int,
|
||||||
|
val chapter: JsonPrimitive,
|
||||||
|
@SerialName("time_stamp") val timestamp: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Chapterization(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Team(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
|
@ -1,315 +1,90 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||||
|
|
||||||
import androidx.preference.PreferenceScreen
|
import android.app.Application
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING
|
import eu.kanade.tachiyomi.multisrc.gmanga.BrowseManga
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_ALL
|
import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_POPULAR
|
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING
|
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA
|
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.dto.TableDto
|
|
||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.dto.asChapterList
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
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.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.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.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import uy.kohesive.injekt.Injekt
|
||||||
import java.util.Locale
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class Gmanga : ConfigurableSource, HttpSource() {
|
class Gmanga : Gmanga(
|
||||||
|
"GMANGA",
|
||||||
private val domain: String = "gmanga.org"
|
"https://gmanga.org",
|
||||||
|
"ar",
|
||||||
override val baseUrl: String = "https://$domain"
|
"https://media.gmanga.me",
|
||||||
|
) {
|
||||||
override val lang: String = "ar"
|
override val client = super.client.newBuilder()
|
||||||
|
|
||||||
override val name: String = "GMANGA"
|
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val preferences = GmangaPreferences(id)
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
|
||||||
.rateLimit(4)
|
.rateLimit(4)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val parsedDatePattern: SimpleDateFormat = SimpleDateFormat(
|
init {
|
||||||
"yyyy-MM-dd HH:mm:ss ZZZ zzz",
|
// remove obsolete preferences
|
||||||
Locale.ENGLISH,
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).run {
|
||||||
|
if (contains("gmanga_chapter_listing")) {
|
||||||
|
edit().remove("gmanga_chapter_listing").apply()
|
||||||
|
}
|
||||||
|
if (contains("gmanga_last_listing")) {
|
||||||
|
edit().remove("gmanga_last_listing").apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val decMga = response.decryptAs<JsonObject>()
|
||||||
|
val selectedManga = decMga["rows"]!!.jsonArray[0].jsonObject["rows"]!!.jsonArray
|
||||||
|
val manags = selectedManga.map {
|
||||||
|
json.decodeFromJsonElement<BrowseManga>(it.jsonArray[17])
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries = manags.map { it.toSManga(::createThumbnail) }
|
||||||
|
.distinctBy { it.url }
|
||||||
|
|
||||||
|
return MangasPage(
|
||||||
|
entries,
|
||||||
|
hasNextPage = (manags.size >= 30),
|
||||||
)
|
)
|
||||||
private val formattedDatePattern: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", USER_AGENT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) =
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
preferences.setupPreferenceScreen(screen)
|
return client.newCall(chapterListRequest(manga))
|
||||||
|
.asObservable() // sites returns false 302 code
|
||||||
|
.map(::chapterListParse)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chaptersRequest(manga: SManga): Request {
|
||||||
val mangaId = manga.url.substringAfterLast("/")
|
val mangaId = manga.url.substringAfterLast("/")
|
||||||
return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
|
return GET("https://api2.gmanga.me/api/mangas/$mangaId/releases", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chaptersParse(response: Response): List<SChapter> {
|
||||||
val data = decryptResponse(response)
|
val chapterList = response.parseAs<ChapterListResponse>()
|
||||||
|
|
||||||
val table = json.decodeFromJsonElement<TableDto>(data)
|
return chapterList.releases.map {
|
||||||
val chapterList = table.asChapterList()
|
|
||||||
|
|
||||||
val releases = when (preferences.getString(PREF_CHAPTER_LISTING)) {
|
|
||||||
PREF_CHAPTER_LISTING_SHOW_POPULAR ->
|
|
||||||
chapterList.releases
|
|
||||||
.groupBy { release -> release.chapterizationId }
|
|
||||||
.mapNotNull { (_, releases) -> releases.maxByOrNull { it.views } }
|
|
||||||
PREF_CHAPTER_LISTING_SHOW_ALL -> chapterList.releases
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return releases.map { release ->
|
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
val chapter = chapterList.chapters.first { it.id == release.chapterizationId }
|
val chapter = chapterList.chapterizations.first { chap -> chap.id == it.chapId }
|
||||||
val team = chapterList.teams.firstOrNull { it.id == release.teamId }
|
val team = chapterList.teams.firstOrNull { team -> team.id == it.teamId }
|
||||||
|
|
||||||
url = "/r/${release.id}"
|
url = "/r/${it.id}"
|
||||||
chapter_number = chapter.chapter
|
chapter_number = it.chapter.float
|
||||||
date_upload = release.timestamp * 1000
|
date_upload = it.timestamp * 1000
|
||||||
scanlator = team?.name
|
scanlator = team?.name
|
||||||
|
|
||||||
val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
|
val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
|
||||||
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
||||||
}
|
}
|
||||||
}.sortedWith(compareBy({ -it.chapter_number }, { -it.date_upload }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val isLatest = when (preferences.getString(PREF_LASTETS_LISTING)) {
|
|
||||||
PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA -> true
|
|
||||||
PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangas = if (!isLatest) {
|
|
||||||
val decMga = decryptResponse(response)
|
|
||||||
val selectedManga = decMga["rows"]!!.jsonArray[0].jsonObject["rows"]!!.jsonArray
|
|
||||||
buildJsonArray {
|
|
||||||
for (i in 0 until selectedManga.size) {
|
|
||||||
add(selectedManga[i].jsonArray[17])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val data = json.decodeFromString<JsonObject>(
|
|
||||||
response.asJsoup().select(".js-react-on-rails-component").html(),
|
|
||||||
)
|
|
||||||
data["mangaDataAction"]!!.jsonObject["newMangas"]!!.jsonArray
|
|
||||||
}
|
|
||||||
return MangasPage(
|
|
||||||
mangas.jsonArray.map {
|
|
||||||
SManga.create().apply {
|
|
||||||
url = "/mangas/${it.jsonObject["id"]!!.jsonPrimitive.content}"
|
|
||||||
title = it.jsonObject["title"]!!.jsonPrimitive.content
|
|
||||||
val thumbnail = "medium_${
|
|
||||||
it.jsonObject["cover"]!!.jsonPrimitive.content.substringBeforeLast(".")
|
|
||||||
}.webp"
|
|
||||||
thumbnail_url =
|
|
||||||
"https://media.gmanga.me/uploads/manga/cover/${it.jsonObject["id"]!!.jsonPrimitive.content}/$thumbnail"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(mangas.size >= 30) && !isLatest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val latestUrl = when (preferences.getString(PREF_LASTETS_LISTING)) {
|
|
||||||
PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA -> "$baseUrl/mangas/latest"
|
|
||||||
PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER -> "https://api.gmanga.me/api/releases?page=$page"
|
|
||||||
else -> "$baseUrl/mangas/latest"
|
|
||||||
}
|
|
||||||
return GET(latestUrl, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val altNamePrefix = "مسميّات أخرى"
|
|
||||||
val translationStatusPrefix = "حالة الترجمة"
|
|
||||||
val startedDayPrefix = "تاريخ النشر"
|
|
||||||
val endedDayPrefix = "تاريخ الانتهاء"
|
|
||||||
val data = json.decodeFromString<JsonObject>(
|
|
||||||
response.asJsoup().select(".js-react-on-rails-component").html(),
|
|
||||||
)
|
|
||||||
val mangaData = data["mangaDataAction"]!!.jsonObject["mangaData"]!!.jsonObject
|
|
||||||
return SManga.create().apply {
|
|
||||||
description =
|
|
||||||
mangaData["summary"]!!.jsonPrimitive.contentOrNull?.ifEmpty { "لم يتم اضافة قصة بعد" }
|
|
||||||
artist =
|
|
||||||
mangaData["artists"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content }
|
|
||||||
author =
|
|
||||||
mangaData["authors"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content }
|
|
||||||
status = parseStatus(mangaData["story_status"].toString())
|
|
||||||
genre = listOfNotNull(
|
|
||||||
mangaData["type"]!!.jsonObject["title"]!!.jsonPrimitive.content,
|
|
||||||
mangaData["type"]!!.jsonObject["name"]!!.jsonPrimitive.content,
|
|
||||||
mangaData["categories"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content },
|
|
||||||
).joinToString(", ")
|
|
||||||
|
|
||||||
parseTranslationStatus(mangaData["translation_status"].toString()).let {
|
|
||||||
description = "$description\n\n:$translationStatusPrefix ᗏ \n$it •"
|
|
||||||
}
|
|
||||||
var startedDate =
|
|
||||||
mangaData["s_date"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() }
|
|
||||||
startedDate = if (startedDate.isNullOrBlank().not()) {
|
|
||||||
parsedDatePattern.parse(startedDate!!)?.let { formattedDatePattern.format(it) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
var endedDay = mangaData["e_date"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() }
|
|
||||||
endedDay = if (endedDay.isNullOrBlank().not()) {
|
|
||||||
parsedDatePattern.parse(endedDay!!)?.let { formattedDatePattern.format(it) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val alternativeName = listOfNotNull(
|
|
||||||
mangaData["synonyms"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
|
|
||||||
mangaData["arabic_title"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
|
|
||||||
mangaData["japanese"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
|
|
||||||
mangaData["english"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
|
|
||||||
).joinToString("\n").trim()
|
|
||||||
|
|
||||||
val additionalInformation = listOfNotNull(
|
|
||||||
startedDate,
|
|
||||||
endedDay,
|
|
||||||
alternativeName,
|
|
||||||
)
|
|
||||||
additionalInformation.forEach { info ->
|
|
||||||
when (info) {
|
|
||||||
startedDate ->
|
|
||||||
description =
|
|
||||||
"$description\n\n:$startedDayPrefix ᗏ \n$startedDate •"
|
|
||||||
endedDay -> description = "$description\n\n:$endedDayPrefix ᗏ \n$endedDay •"
|
|
||||||
alternativeName ->
|
|
||||||
description =
|
|
||||||
"$description\n\n:$altNamePrefix ᗏ \n$alternativeName •"
|
|
||||||
else -> description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?) = when {
|
|
||||||
status == null -> SManga.UNKNOWN
|
|
||||||
status.contains("2") -> SManga.ONGOING
|
|
||||||
status.contains("3") -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseTranslationStatus(status: String?) = when {
|
|
||||||
status == null -> "مجهول"
|
|
||||||
status.contains("0") -> "منتهية"
|
|
||||||
status.contains("1") -> "مستمرة"
|
|
||||||
status.contains("2") -> "متوقفة"
|
|
||||||
else -> "مجهول"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val url = response.request.url.toString()
|
|
||||||
val data = json.decodeFromString<JsonObject>(
|
|
||||||
response.asJsoup().select(".js-react-on-rails-component").html(),
|
|
||||||
)
|
|
||||||
val releaseData =
|
|
||||||
data["readerDataAction"]!!.jsonObject["readerData"]!!.jsonObject["release"]!!.jsonObject
|
|
||||||
|
|
||||||
val hasWebP = releaseData["webp_pages"]!!.jsonArray.size > 0
|
|
||||||
return releaseData[if (hasWebP) "webp_pages" else "pages"]!!.jsonArray.map { it.jsonPrimitive.content }
|
|
||||||
.sortedWith(pageSort)
|
|
||||||
.mapIndexed { index, pageUri ->
|
|
||||||
Page(
|
|
||||||
index,
|
|
||||||
"$url#page_$index",
|
|
||||||
"https://media.gmanga.me/uploads/releases/${releaseData["storage_key"]!!.jsonPrimitive.content}/hq${if (hasWebP) "_webp" else ""}/$pageUri",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pageSort =
|
|
||||||
compareBy<String>({ parseNumber(0, it) ?: Double.MAX_VALUE }, { parseNumber(1, it) }, { parseNumber(2, it) })
|
|
||||||
|
|
||||||
private fun parseNumber(index: Int, string: String): Double? =
|
|
||||||
Regex("\\d+").findAll(string).map { it.value }.toList().getOrNull(index)?.toDoubleOrNull()
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val data = decryptResponse(response)
|
|
||||||
val mangas = data["mangas"]!!.jsonArray
|
|
||||||
return MangasPage(
|
|
||||||
mangas.jsonArray.map {
|
|
||||||
SManga.create().apply {
|
|
||||||
url = "/mangas/${it.jsonObject["id"]!!.jsonPrimitive.content}"
|
|
||||||
title = it.jsonObject["title"]!!.jsonPrimitive.content
|
|
||||||
val thumbnail = "medium_${
|
|
||||||
it.jsonObject["cover"]!!.jsonPrimitive.content.substringBeforeLast(".")
|
|
||||||
}.webp"
|
|
||||||
thumbnail_url =
|
|
||||||
"https://media.gmanga.me/uploads/manga/cover/${it.jsonObject["id"]!!.jsonPrimitive.content}/$thumbnail"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mangas.size == 50,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decryptResponse(response: Response): JsonObject {
|
|
||||||
val encryptedData =
|
|
||||||
json.decodeFromString<JsonObject>(response.body.string())["data"]!!.jsonPrimitive.content
|
|
||||||
val decryptedData = decrypt(encryptedData)
|
|
||||||
return json.decodeFromString(decryptedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return GmangaFilters.buildSearchPayload(
|
|
||||||
page,
|
|
||||||
query,
|
|
||||||
if (filters.isEmpty()) getFilterList() else filters,
|
|
||||||
).let {
|
|
||||||
val body = it.toString().toRequestBody(MEDIA_TYPE)
|
|
||||||
POST("$baseUrl/api/mangas/search", headers, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = GmangaFilters.getFilterList()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val USER_AGENT =
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36"
|
|
||||||
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,291 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import kotlinx.serialization.json.JsonNull
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import kotlinx.serialization.json.putJsonArray
|
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
class GmangaFilters() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun getFilterList() = FilterList(
|
|
||||||
MangaTypeFilter(),
|
|
||||||
OneShotFilter(),
|
|
||||||
StoryStatusFilter(),
|
|
||||||
TranslationStatusFilter(),
|
|
||||||
ChapterCountFilter(),
|
|
||||||
DateRangeFilter(),
|
|
||||||
CategoryFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun buildSearchPayload(page: Int, query: String = "", filters: FilterList): JsonObject {
|
|
||||||
val mangaTypeFilter = filters.findInstance<MangaTypeFilter>()!!
|
|
||||||
val oneShotFilter = filters.findInstance<OneShotFilter>()!!
|
|
||||||
val storyStatusFilter = filters.findInstance<StoryStatusFilter>()!!
|
|
||||||
val translationStatusFilter = filters.findInstance<TranslationStatusFilter>()!!
|
|
||||||
val chapterCountFilter = filters.findInstance<ChapterCountFilter>()!!
|
|
||||||
val dateRangeFilter = filters.findInstance<DateRangeFilter>()!!
|
|
||||||
val categoryFilter = filters.findInstance<CategoryFilter>()!!
|
|
||||||
|
|
||||||
return buildJsonObject {
|
|
||||||
oneShotFilter.state.first().let {
|
|
||||||
putJsonObject("oneshot") {
|
|
||||||
when {
|
|
||||||
it.isIncluded() -> put("value", true)
|
|
||||||
it.isExcluded() -> put("value", false)
|
|
||||||
else -> put("value", JsonNull)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
put("title", query)
|
|
||||||
put("page", page)
|
|
||||||
putJsonObject("manga_types") {
|
|
||||||
putJsonArray("include") {
|
|
||||||
mangaTypeFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
putJsonArray("exclude") {
|
|
||||||
mangaTypeFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putJsonObject("story_status") {
|
|
||||||
putJsonArray("include") {
|
|
||||||
storyStatusFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
putJsonArray("exclude") {
|
|
||||||
storyStatusFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putJsonObject("translation_status") {
|
|
||||||
putJsonArray("include") {
|
|
||||||
translationStatusFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
putJsonArray("exclude") {
|
|
||||||
translationStatusFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putJsonObject("categories") {
|
|
||||||
putJsonArray("include") {
|
|
||||||
add(JsonNull) // always included, maybe to avoid shifting index in the backend
|
|
||||||
categoryFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
putJsonArray("exclude") {
|
|
||||||
categoryFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putJsonObject("chapters") {
|
|
||||||
putFromValidatingTextFilter(
|
|
||||||
chapterCountFilter.state.first {
|
|
||||||
it.id == FILTER_ID_MIN_CHAPTER_COUNT
|
|
||||||
},
|
|
||||||
"min",
|
|
||||||
ERROR_INVALID_MIN_CHAPTER_COUNT,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
putFromValidatingTextFilter(
|
|
||||||
chapterCountFilter.state.first {
|
|
||||||
it.id == FILTER_ID_MAX_CHAPTER_COUNT
|
|
||||||
},
|
|
||||||
"max",
|
|
||||||
ERROR_INVALID_MAX_CHAPTER_COUNT,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
putJsonObject("dates") {
|
|
||||||
putFromValidatingTextFilter(
|
|
||||||
dateRangeFilter.state.first {
|
|
||||||
it.id == FILTER_ID_START_DATE
|
|
||||||
},
|
|
||||||
"start",
|
|
||||||
ERROR_INVALID_START_DATE,
|
|
||||||
)
|
|
||||||
|
|
||||||
putFromValidatingTextFilter(
|
|
||||||
dateRangeFilter.state.first {
|
|
||||||
it.id == FILTER_ID_END_DATE
|
|
||||||
},
|
|
||||||
"end",
|
|
||||||
ERROR_INVALID_END_DATE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter IDs
|
|
||||||
private const val FILTER_ID_ONE_SHOT = "oneshot"
|
|
||||||
private const val FILTER_ID_START_DATE = "start"
|
|
||||||
private const val FILTER_ID_END_DATE = "end"
|
|
||||||
private const val FILTER_ID_MIN_CHAPTER_COUNT = "min"
|
|
||||||
private const val FILTER_ID_MAX_CHAPTER_COUNT = "max"
|
|
||||||
|
|
||||||
// error messages
|
|
||||||
private const val ERROR_INVALID_START_DATE = "تاريخ بداية غير صالح"
|
|
||||||
private const val ERROR_INVALID_END_DATE = " تاريخ نهاية غير صالح"
|
|
||||||
private const val ERROR_INVALID_MIN_CHAPTER_COUNT = "الحد الأدنى لعدد الفصول غير صالح"
|
|
||||||
private const val ERROR_INVALID_MAX_CHAPTER_COUNT = "الحد الأقصى لعدد الفصول غير صالح"
|
|
||||||
|
|
||||||
private class MangaTypeFilter() : Filter.Group<TagFilter>(
|
|
||||||
"الأصل",
|
|
||||||
listOf(
|
|
||||||
TagFilter("1", "يابانية", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("2", "كورية", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("3", "صينية", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("4", "عربية", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("5", "كوميك", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("6", "هواة", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("7", "إندونيسية", TriState.STATE_INCLUDE),
|
|
||||||
TagFilter("8", "روسية", TriState.STATE_INCLUDE),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class OneShotFilter() : Filter.Group<TagFilter>(
|
|
||||||
"ونشوت؟",
|
|
||||||
listOf(
|
|
||||||
TagFilter(FILTER_ID_ONE_SHOT, "نعم", TriState.STATE_EXCLUDE),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class StoryStatusFilter() : Filter.Group<TagFilter>(
|
|
||||||
"حالة القصة",
|
|
||||||
listOf(
|
|
||||||
TagFilter("2", "مستمرة"),
|
|
||||||
TagFilter("3", "منتهية"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class TranslationStatusFilter() : Filter.Group<TagFilter>(
|
|
||||||
"حالة الترجمة",
|
|
||||||
listOf(
|
|
||||||
TagFilter("0", "منتهية"),
|
|
||||||
TagFilter("1", "مستمرة"),
|
|
||||||
TagFilter("2", "متوقفة"),
|
|
||||||
TagFilter("3", "غير مترجمة", TriState.STATE_EXCLUDE),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class ChapterCountFilter() : Filter.Group<IntFilter>(
|
|
||||||
"عدد الفصول",
|
|
||||||
listOf(
|
|
||||||
IntFilter(FILTER_ID_MIN_CHAPTER_COUNT, "على الأقل"),
|
|
||||||
IntFilter(FILTER_ID_MAX_CHAPTER_COUNT, "على الأكثر"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class DateRangeFilter() : Filter.Group<DateFilter>(
|
|
||||||
"تاريخ النشر",
|
|
||||||
listOf(
|
|
||||||
DateFilter(FILTER_ID_START_DATE, "تاريخ النشر"),
|
|
||||||
DateFilter(FILTER_ID_END_DATE, "تاريخ الإنتهاء"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class CategoryFilter() : Filter.Group<TagFilter>(
|
|
||||||
"التصنيفات",
|
|
||||||
listOf(
|
|
||||||
TagFilter("1", "إثارة"),
|
|
||||||
TagFilter("2", "أكشن"),
|
|
||||||
TagFilter("3", "الحياة المدرسية"),
|
|
||||||
TagFilter("4", "الحياة اليومية"),
|
|
||||||
TagFilter("5", "آليات"),
|
|
||||||
TagFilter("6", "تاريخي"),
|
|
||||||
TagFilter("7", "تراجيدي"),
|
|
||||||
TagFilter("8", "جوسيه"),
|
|
||||||
TagFilter("9", "حربي"),
|
|
||||||
TagFilter("10", "خيال"),
|
|
||||||
TagFilter("11", "خيال علمي"),
|
|
||||||
TagFilter("12", "دراما"),
|
|
||||||
TagFilter("13", "رعب"),
|
|
||||||
TagFilter("14", "رومانسي"),
|
|
||||||
TagFilter("15", "رياضة"),
|
|
||||||
TagFilter("16", "ساموراي"),
|
|
||||||
TagFilter("17", "سحر"),
|
|
||||||
TagFilter("18", "سينين"),
|
|
||||||
TagFilter("19", "شوجو"),
|
|
||||||
TagFilter("20", "شونين"),
|
|
||||||
TagFilter("21", "عنف"),
|
|
||||||
TagFilter("22", "غموض"),
|
|
||||||
TagFilter("23", "فنون قتال"),
|
|
||||||
TagFilter("24", "قوى خارقة"),
|
|
||||||
TagFilter("25", "كوميدي"),
|
|
||||||
TagFilter("26", "لعبة"),
|
|
||||||
TagFilter("27", "مسابقة"),
|
|
||||||
TagFilter("28", "مصاصي الدماء"),
|
|
||||||
TagFilter("29", "مغامرات"),
|
|
||||||
TagFilter("30", "موسيقى"),
|
|
||||||
TagFilter("31", "نفسي"),
|
|
||||||
TagFilter("32", "نينجا"),
|
|
||||||
TagFilter("33", "وحوش"),
|
|
||||||
TagFilter("34", "حريم"),
|
|
||||||
TagFilter("35", "راشد"),
|
|
||||||
TagFilter("38", "ويب-تون"),
|
|
||||||
TagFilter("39", "زمنكاني"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val DATE_FILTER_PATTERN = "yyyy/MM/dd"
|
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
|
||||||
private val DATE_FITLER_FORMAT = SimpleDateFormat(DATE_FILTER_PATTERN).apply {
|
|
||||||
isLenient = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SimpleDateFormat.isValid(date: String): Boolean {
|
|
||||||
return try {
|
|
||||||
this.parse(date)
|
|
||||||
true
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JsonObjectBuilder.putFromValidatingTextFilter(
|
|
||||||
filter: ValidatingTextFilter,
|
|
||||||
property: String,
|
|
||||||
invalidErrorMessage: String,
|
|
||||||
default: String? = null,
|
|
||||||
) {
|
|
||||||
filter.let {
|
|
||||||
when {
|
|
||||||
it.state == "" -> if (default == null) {
|
|
||||||
put(property, JsonNull)
|
|
||||||
} else {
|
|
||||||
put(property, default)
|
|
||||||
}
|
|
||||||
it.isValid() -> put(property, it.state)
|
|
||||||
else -> throw Exception(invalidErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
|
||||||
|
|
||||||
private class TagFilter(val id: String, name: String, state: Int = STATE_IGNORE) : Filter.TriState(name, state)
|
|
||||||
|
|
||||||
private abstract class ValidatingTextFilter(name: String) : Filter.Text(name) {
|
|
||||||
abstract fun isValid(): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DateFilter(val id: String, name: String) : ValidatingTextFilter("($DATE_FILTER_PATTERN) $name)") {
|
|
||||||
override fun isValid(): Boolean = DATE_FITLER_FORMAT.isValid(this.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class IntFilter(val id: String, name: String) : ValidatingTextFilter(name) {
|
|
||||||
override fun isValid(): Boolean = state.toIntOrNull() != null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class GmangaPreferences(id: Long) {
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
STRING_PREFERENCES.forEach {
|
|
||||||
val preference = ListPreference(screen.context).apply {
|
|
||||||
key = it.key
|
|
||||||
title = it.title
|
|
||||||
entries = it.entries()
|
|
||||||
entryValues = it.entryValues()
|
|
||||||
summary = "%s"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preferences.contains(it.key)) {
|
|
||||||
preferences.edit().putString(it.key, it.default().key).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getString(pref: StringPreference): String {
|
|
||||||
return preferences.getString(pref.key, pref.default().key)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
class StringPreferenceOption(val key: String, val title: String)
|
|
||||||
|
|
||||||
class StringPreference(
|
|
||||||
val key: String,
|
|
||||||
val title: String,
|
|
||||||
private val options: List<StringPreferenceOption>,
|
|
||||||
private val defaultOptionIndex: Int = 0,
|
|
||||||
) {
|
|
||||||
fun entries(): Array<String> = options.map { it.title }.toTypedArray()
|
|
||||||
fun entryValues(): Array<String> = options.map { it.key }.toTypedArray()
|
|
||||||
fun default(): StringPreferenceOption = options[defaultOptionIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
// preferences
|
|
||||||
const val PREF_CHAPTER_LISTING_SHOW_ALL = "gmanga_gmanga_chapter_listing_show_all"
|
|
||||||
const val PREF_CHAPTER_LISTING_SHOW_POPULAR = "gmanga_chapter_listing_most_viewed"
|
|
||||||
const val PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER = "gmanga_Last_listing_last_chapter_added"
|
|
||||||
const val PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA = "gmanga_chapter_listing_last_manga_added"
|
|
||||||
|
|
||||||
val PREF_CHAPTER_LISTING = StringPreference(
|
|
||||||
"gmanga_chapter_listing",
|
|
||||||
"كيفية عرض الفصل بقائمة الفصول",
|
|
||||||
listOf(
|
|
||||||
StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_POPULAR, "اختيار النسخة الأكثر مشاهدة"),
|
|
||||||
StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_ALL, "عرض جميع النسخ"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val PREF_LASTETS_LISTING = StringPreference(
|
|
||||||
"gmanga_last_listing",
|
|
||||||
"كيفية عرض بقائمة الأعمال الجديدة ",
|
|
||||||
listOf(
|
|
||||||
StringPreferenceOption(PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER, "اختيار آخر الإضافات"),
|
|
||||||
StringPreferenceOption(PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA, "اختيار لمانجات الجديدة"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val STRING_PREFERENCES = listOf(
|
|
||||||
PREF_CHAPTER_LISTING,
|
|
||||||
PREF_LASTETS_LISTING,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ChapterDto(
|
|
||||||
val id: Int,
|
|
||||||
val chapter: Float,
|
|
||||||
val volume: Int,
|
|
||||||
val title: String,
|
|
||||||
@SerialName("time_stamp") val timestamp: Long,
|
|
||||||
)
|
|
|
@ -1,10 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ChapterListDto(
|
|
||||||
val releases: List<ReleaseDto>,
|
|
||||||
val teams: List<TeamDto>,
|
|
||||||
val chapters: List<ChapterDto>,
|
|
||||||
)
|
|
|
@ -1,15 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReleaseDto(
|
|
||||||
val id: Int,
|
|
||||||
@SerialName("created_at") val createdAt: String,
|
|
||||||
@SerialName("timestamp") val timestamp: Long,
|
|
||||||
val views: Int,
|
|
||||||
@SerialName("chapterization_id") val chapterizationId: Int,
|
|
||||||
@SerialName("team_id") val teamId: Int,
|
|
||||||
val teams: List<Int>,
|
|
||||||
)
|
|
|
@ -1,61 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
|
||||||
import kotlinx.serialization.json.float
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TableDto(
|
|
||||||
val cols: List<String>,
|
|
||||||
val rows: List<JsonElement>,
|
|
||||||
val isCompact: Boolean,
|
|
||||||
val maxLevel: Int,
|
|
||||||
val isArray: Boolean? = null,
|
|
||||||
val isObject: Boolean? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private fun TableDto.get(key: String): TableDto? {
|
|
||||||
isObject ?: return null
|
|
||||||
|
|
||||||
val index = cols.indexOf(key)
|
|
||||||
return json.decodeFromJsonElement(rows[index])
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TableDto.asChapterList() = ChapterListDto(
|
|
||||||
// YOLO
|
|
||||||
get("releases")!!.rows.map {
|
|
||||||
ReleaseDto(
|
|
||||||
it.jsonArray[0].jsonPrimitive.int,
|
|
||||||
it.jsonArray[1].jsonPrimitive.content,
|
|
||||||
it.jsonArray[2].jsonPrimitive.long,
|
|
||||||
it.jsonArray[3].jsonPrimitive.int,
|
|
||||||
it.jsonArray[4].jsonPrimitive.int,
|
|
||||||
it.jsonArray[5].jsonPrimitive.int,
|
|
||||||
it.jsonArray[6].jsonArray.map { it.jsonPrimitive.int },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
get("teams")!!.rows.map {
|
|
||||||
TeamDto(
|
|
||||||
it.jsonArray[0].jsonPrimitive.int,
|
|
||||||
it.jsonArray[1].jsonPrimitive.content,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
get("chapterizations")!!.rows.map {
|
|
||||||
ChapterDto(
|
|
||||||
it.jsonArray[0].jsonPrimitive.int,
|
|
||||||
it.jsonArray[1].jsonPrimitive.float,
|
|
||||||
it.jsonArray[2].jsonPrimitive.int,
|
|
||||||
it.jsonArray[3].jsonPrimitive.content,
|
|
||||||
it.jsonArray[4].jsonPrimitive.long,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TeamDto(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Hentai Slayer'
|
||||||
|
extClass = '.HentaiSlayer'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,241 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.hentaislayer
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
|
override val name = "هنتاي سلاير"
|
||||||
|
|
||||||
|
override val baseUrl = "https://hentaislayer.net"
|
||||||
|
|
||||||
|
override val lang = "ar"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
.set("Origin", baseUrl)
|
||||||
|
|
||||||
|
private val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?page=$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div > div:has(div#card-real)"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
element.selectFirst("div#card-real a")?.run {
|
||||||
|
setUrlWithoutDomain(absUrl("href"))
|
||||||
|
selectFirst("figure")?.run {
|
||||||
|
selectFirst("img.object-cover")?.run {
|
||||||
|
thumbnail_url = imgAttr()
|
||||||
|
title = attr("alt")
|
||||||
|
}
|
||||||
|
genre = select("span p.drop-shadow-sm").text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)"
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/latest-${getLatestTypes()}?page=$page", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/manga?title=$query".toHttpUrl().newBuilder()
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||||
|
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
|
||||||
|
is GenresFilter ->
|
||||||
|
filter.state
|
||||||
|
.filter { it.state }
|
||||||
|
.forEach { url.addQueryParameter("genre[]", it.uriPart) }
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url.addQueryParameter("page", page.toString())
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
document.selectFirst("main section")?.run {
|
||||||
|
selectFirst("img#manga-cover")?.run {
|
||||||
|
thumbnail_url = imgAttr()
|
||||||
|
title = attr("alt")
|
||||||
|
}
|
||||||
|
selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")?.run {
|
||||||
|
status = parseStatus(select("a[href*='?status=']").text())
|
||||||
|
genre = select("a[href*='?type=']").text()
|
||||||
|
author = select("p:has(span:contains(المؤلف)) span:nth-child(2)").text()
|
||||||
|
artist = select("p:has(span:contains(الرسام)) span:nth-child(2)").text()
|
||||||
|
}
|
||||||
|
selectFirst("section > div:nth-child(1) > div:nth-child(2)")?.run {
|
||||||
|
select("h1").text().takeIf { it.isNotEmpty() }?.let {
|
||||||
|
title = it
|
||||||
|
}
|
||||||
|
genre = select("a[href*='?genre=']")
|
||||||
|
.map { it.text() }
|
||||||
|
.let {
|
||||||
|
listOf(genre) + it
|
||||||
|
}
|
||||||
|
.joinToString()
|
||||||
|
select("h2").text().takeIf { it.isNotEmpty() }?.let {
|
||||||
|
description = "Alternative name: $it\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description += select("#description").text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("مستمر") -> SManga.ONGOING
|
||||||
|
status.contains("متوقف") -> SManga.CANCELLED
|
||||||
|
status.contains("مكتمل") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
override fun chapterListSelector() = "main section #chapters-list a#chapter-item"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
name = "\u061C" + element.select("#item-title").text() // Add unicode ARABIC LETTER MARK to ensure all titles are right to left
|
||||||
|
|
||||||
|
date_upload = parseRelativeDate(element.select("#item-title + span").text()) ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses dates in this form:
|
||||||
|
* `11 days ago`
|
||||||
|
*/
|
||||||
|
private fun parseRelativeDate(date: String): Long? {
|
||||||
|
val trimmedDate = date.split(" ")
|
||||||
|
|
||||||
|
if (trimmedDate[2] != "ago") return null
|
||||||
|
|
||||||
|
val number = trimmedDate[0].toIntOrNull() ?: return null
|
||||||
|
val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix
|
||||||
|
|
||||||
|
val now = Calendar.getInstance()
|
||||||
|
|
||||||
|
// Map English unit to Java unit
|
||||||
|
val javaUnit = when (unit) {
|
||||||
|
"year", "yr" -> Calendar.YEAR
|
||||||
|
"month" -> Calendar.MONTH
|
||||||
|
"week", "wk" -> Calendar.WEEK_OF_MONTH
|
||||||
|
"day" -> Calendar.DAY_OF_MONTH
|
||||||
|
"hour", "hr" -> Calendar.HOUR
|
||||||
|
"minute", "min" -> Calendar.MINUTE
|
||||||
|
"second", "sec" -> Calendar.SECOND
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
now.add(javaUnit, -number)
|
||||||
|
|
||||||
|
return now.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("img.chapter-image").mapIndexed { index, item ->
|
||||||
|
Page(index = index, imageUrl = item.imgAttr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private fun Element.imgAttr(): String? {
|
||||||
|
return when {
|
||||||
|
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
|
||||||
|
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
GenresFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================== Settings ==============================
|
||||||
|
companion object {
|
||||||
|
private const val LATEST_PREF = "LatestType"
|
||||||
|
private val LATEST_PREF_ENTRIES get() = arrayOf(
|
||||||
|
"مانجا",
|
||||||
|
"مانهوا",
|
||||||
|
"كوميكس",
|
||||||
|
)
|
||||||
|
private val LATEST_PREF_ENTRY_VALUES get() = arrayOf(
|
||||||
|
"manga",
|
||||||
|
"manhwa",
|
||||||
|
"comics",
|
||||||
|
)
|
||||||
|
private val LATEST_PREF_DEFAULT = LATEST_PREF_ENTRY_VALUES[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = LATEST_PREF
|
||||||
|
title = "نوع القائمة الأحدث"
|
||||||
|
summary = "حدد نوع الإدخالات التي سيتم الاستعلام عنها لأحدث قائمة. الأنواع الأخرى متوفرة في الشائع/التصفح أو البحث"
|
||||||
|
entries = LATEST_PREF_ENTRIES
|
||||||
|
entryValues = LATEST_PREF_ENTRY_VALUES
|
||||||
|
setDefaultValue(LATEST_PREF_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, _ ->
|
||||||
|
Toast.makeText(
|
||||||
|
screen.context,
|
||||||
|
".لتطبيق الإعدادات الجديدة Tachiyomi أعد تشغيل",
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLatestTypes(): String = preferences.getString(LATEST_PREF, LATEST_PREF_DEFAULT)!!
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.hentaislayer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class StatusFilter : UriPartFilter(
|
||||||
|
"الحالة",
|
||||||
|
arrayOf(
|
||||||
|
Pair("الكل", ""),
|
||||||
|
Pair("مستمر", "مستمر"),
|
||||||
|
Pair("متوقف", "متوقف"),
|
||||||
|
Pair("مكتمل", "مكتمل"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TypeFilter : UriPartFilter(
|
||||||
|
"النوع",
|
||||||
|
arrayOf(
|
||||||
|
Pair("الكل", ""),
|
||||||
|
Pair("مانجا", "مانجا"),
|
||||||
|
Pair("مانهوا", "مانهوا"),
|
||||||
|
Pair("كوميكس", "كوميكس"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val genres = listOf(
|
||||||
|
Genre("أكشن", "أكشن"),
|
||||||
|
Genre("ألعاب جنسية", "ألعاب جنسية"),
|
||||||
|
Genre("إذلال", "إذلال"),
|
||||||
|
Genre("إيلف", "إيلف"),
|
||||||
|
Genre("ابتزاز", "ابتزاز"),
|
||||||
|
Genre("استعباد", "استعباد"),
|
||||||
|
Genre("اغتصاب", "اغتصاب"),
|
||||||
|
Genre("بدون حجب", "بدون حجب"),
|
||||||
|
Genre("بشرة سمراء", "بشرة سمراء"),
|
||||||
|
Genre("تاريخي", "تاريخي"),
|
||||||
|
Genre("تحكم بالعقل", "تحكم بالعقل"),
|
||||||
|
Genre("تراب", "تراب"),
|
||||||
|
Genre("تسوندري", "تسوندري"),
|
||||||
|
Genre("تصوير", "تصوير"),
|
||||||
|
Genre("جنس بالقدم", "جنس بالقدم"),
|
||||||
|
Genre("جنس جماعي", "جنس جماعي"),
|
||||||
|
Genre("جنس شرجي", "جنس شرجي"),
|
||||||
|
Genre("حريم", "حريم"),
|
||||||
|
Genre("حمل", "حمل"),
|
||||||
|
Genre("خادمة", "خادمة"),
|
||||||
|
Genre("خيال", "خيال"),
|
||||||
|
Genre("خيانة", "خيانة"),
|
||||||
|
Genre("دراغون بول", "دراغون بول"),
|
||||||
|
Genre("دراما", "دراما"),
|
||||||
|
Genre("رومانسي", "رومانسي"),
|
||||||
|
Genre("سحر", "سحر"),
|
||||||
|
Genre("شوتا", "شوتا"),
|
||||||
|
Genre("شيطانة", "شيطانة"),
|
||||||
|
Genre("شيميل", "شيميل"),
|
||||||
|
Genre("طالبة مدرسة", "طالبة مدرسة"),
|
||||||
|
Genre("عمة", "عمة"),
|
||||||
|
Genre("فوتا", "فوتا"),
|
||||||
|
Genre("لولي", "لولي"),
|
||||||
|
Genre("محارم", "محارم"),
|
||||||
|
Genre("مدرسي", "مدرسي"),
|
||||||
|
Genre("مكان عام", "مكان عام"),
|
||||||
|
Genre("ملون", "ملون"),
|
||||||
|
Genre("ميلف", "ميلف"),
|
||||||
|
Genre("ناروتو", "ناروتو"),
|
||||||
|
Genre("هجوم العمالقة", "هجوم العمالقة"),
|
||||||
|
Genre("ون بيس", "ون بيس"),
|
||||||
|
Genre("ياوي", "ياوي"),
|
||||||
|
Genre("يوري", "يوري"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Genre(val name: String, val uriPart: String)
|
||||||
|
|
||||||
|
class GenreCheckBox(name: String, val uriPart: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
class GenresFilter :
|
||||||
|
Filter.Group<GenreCheckBox>("التصنيفات", genres.map { GenreCheckBox(it.name, it.uriPart) })
|
||||||
|
|
||||||
|
open class UriPartFilter(displayName: String, private val pairs: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, pairs.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = pairs[state].second
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'MangaTak'
|
||||||
|
extClass = '.MangaTak'
|
||||||
|
themePkg = 'mangathemesia'
|
||||||
|
baseUrl = 'https://mangatak.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,12 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.mangatak
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaTak : MangaThemesia(
|
||||||
|
"MangaTak",
|
||||||
|
"https://mangatak.com",
|
||||||
|
"ar",
|
||||||
|
dateFormat = SimpleDateFormat("MMMM DD, yyyy", Locale("ar")),
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Manga Tales'
|
||||||
|
extClass = '.MangaTales'
|
||||||
|
themePkg = 'gmanga'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.mangatales
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListDto(
|
||||||
|
val mangaReleases: List<ChapterRelease>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterRelease(
|
||||||
|
private val id: Int,
|
||||||
|
private val chapter: JsonPrimitive,
|
||||||
|
private val title: String,
|
||||||
|
@SerialName("team_name") private val teamName: String,
|
||||||
|
@SerialName("created_at") private val createdAt: String,
|
||||||
|
) {
|
||||||
|
fun toSChapter() = SChapter.create().apply {
|
||||||
|
url = "/r/$id"
|
||||||
|
chapter_number = chapter.float
|
||||||
|
date_upload = try {
|
||||||
|
dateFormat.parse(createdAt)!!.time
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
scanlator = teamName
|
||||||
|
|
||||||
|
val chapterName = title.let { if (it.trim() != "") " - $it" else "" }
|
||||||
|
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderDto(
|
||||||
|
val readerDataAction: ReaderData,
|
||||||
|
val globals: Globals,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Globals(
|
||||||
|
val mediaKey: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderData(
|
||||||
|
val readerData: ReaderChapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderChapter(
|
||||||
|
val release: ReaderPages,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ReaderPages(
|
||||||
|
@SerialName("hq_pages") private val page: String,
|
||||||
|
) {
|
||||||
|
val pages get() = page.split("\r\n")
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.mangatales
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gmanga.TagFilterData
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
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.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class MangaTales : Gmanga(
|
||||||
|
"Manga Tales",
|
||||||
|
"https://www.mangatales.com",
|
||||||
|
"ar",
|
||||||
|
"https://media.mangatales.com",
|
||||||
|
) {
|
||||||
|
override fun createThumbnail(mangaId: String, cover: String): String {
|
||||||
|
return "$cdnUrl/uploads/manga/cover/$mangaId/large_$cover"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTypesFilter() = listOf(
|
||||||
|
TagFilterData("1", "عربية", Filter.TriState.STATE_INCLUDE),
|
||||||
|
TagFilterData("2", "إنجليزي", Filter.TriState.STATE_INCLUDE),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun chaptersRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.substringAfterLast("/")
|
||||||
|
return GET("$baseUrl/api/mangas/$mangaId", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chaptersParse(response: Response): List<SChapter> {
|
||||||
|
val releases = response.parseAs<ChapterListDto>().mangaReleases
|
||||||
|
|
||||||
|
return releases.map { it.toSChapter() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val data = response.asJsoup()
|
||||||
|
.select(".js-react-on-rails-component").html()
|
||||||
|
.parseAs<ReaderDto>()
|
||||||
|
|
||||||
|
return data.readerDataAction.readerData.release.pages
|
||||||
|
.mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = "$cdnUrl/uploads/releases/$img?ak=${data.globals.mediaKey}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Hentairead'
|
extClass = '.Hentairead'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://hentairead.com'
|
baseUrl = 'https://hentairead.com'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,16 @@ package eu.kanade.tachiyomi.extension.en.hentairead
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
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 okhttp3.Response
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -13,6 +19,22 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
|
|
||||||
override val versionId: Int = 2
|
override val versionId: Int = 2
|
||||||
|
|
||||||
|
override val mangaSubString = "hentai"
|
||||||
|
override val fetchGenres = false
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList()
|
||||||
|
|
||||||
|
override fun searchLoadMoreRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl${searchPage(page)}".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("s", query)
|
||||||
|
.addQueryParameter("post_type", "wp-manga")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.c-tabs-item div.page-item-detail"
|
||||||
|
|
||||||
override val mangaDetailsSelectorDescription = "div.post-sub-title.alt-title > h2"
|
override val mangaDetailsSelectorDescription = "div.post-sub-title.alt-title > h2"
|
||||||
override val mangaDetailsSelectorAuthor = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
override val mangaDetailsSelectorAuthor = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
||||||
override val mangaDetailsSelectorArtist = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
override val mangaDetailsSelectorArtist = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
||||||
|
@ -21,6 +43,13 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
|
|
||||||
override val pageListParseSelector = "li.chapter-image-item > a > div.image-wrapper"
|
override val pageListParseSelector = "li.chapter-image-item > a > div.image-wrapper"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
return super.mangaDetailsParse(document).apply {
|
||||||
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
launchIO { countViews(document) }
|
launchIO { countViews(document) }
|
||||||
|
|
||||||
|
@ -37,12 +66,14 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return listOf(
|
return Observable.just(
|
||||||
|
listOf(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
name = "Chapter"
|
name = "Chapter"
|
||||||
setUrlWithoutDomain(response.request.url.encodedPath)
|
url = manga.url
|
||||||
},
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'I Roved Out'
|
extName = 'I Roved Out'
|
||||||
extClass = '.IRovedOut'
|
extClass = '.IRovedOut'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,8 +90,6 @@ class IRovedOut : HttpSource() {
|
||||||
return Observable.just(pages)
|
return Observable.just(pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manga Demon'
|
extName = 'Manga Demon'
|
||||||
extClass = '.MangaDemon'
|
extClass = '.MangaDemon'
|
||||||
extVersionCode = 9
|
extVersionCode = 10
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class MangaDemon : ParsedHttpSource() {
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
override val name = "Manga Demon"
|
override val name = "Manga Demon"
|
||||||
override val baseUrl = "https://demontoon.com"
|
override val baseUrl = "https://demonreader.org"
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.rateLimit(1)
|
.rateLimit(1)
|
||||||
|
@ -195,7 +195,7 @@ class MangaDemon : ParsedHttpSource() {
|
||||||
|
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
title = infoElement.select("h1.novel-title").text()
|
title = infoElement.select("h1.novel-title").text()
|
||||||
author = infoElement.select("div.author").text().drop(7)
|
author = infoElement.select("div.author > [itemprop=author]").text()
|
||||||
status = parseStatus(infoElement.select("span:has(small:containsOwn(Status))").text())
|
status = parseStatus(infoElement.select("span:has(small:containsOwn(Status))").text())
|
||||||
genre = infoElement.select("a.property-item").joinToString { it.text() }
|
genre = infoElement.select("a.property-item").joinToString { it.text() }
|
||||||
description = infoElement.select("p.description").text()
|
description = infoElement.select("p.description").text()
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.MangaDistrict'
|
extClass = '.MangaDistrict'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://mangadistrict.com'
|
baseUrl = 'https://mangadistrict.com'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,13 @@ class MangaDistrict :
|
||||||
),
|
),
|
||||||
ConfigurableSource {
|
ConfigurableSource {
|
||||||
|
|
||||||
|
override val mangaSubString = "read-scan"
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div[role=navigation] span.current + a.page"
|
override fun popularMangaNextPageSelector() = "div[role=navigation] span.current + a.page"
|
||||||
|
|
||||||
private val titleVersion = Regex("\\(.*\\)")
|
private val titleVersion = Regex("\\(.*\\)")
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Mangafreak'
|
extName = 'Mangafreak'
|
||||||
extClass = '.Mangafreak'
|
extClass = '.Mangafreak'
|
||||||
extVersionCode = 9
|
extVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Mangafreak : ParsedHttpSource() {
|
||||||
|
|
||||||
override val lang: String = "en"
|
override val lang: String = "en"
|
||||||
|
|
||||||
override val baseUrl: String = "https://w15.mangafreak.net"
|
override val baseUrl: String = "https://ww1.mangafreak.me"
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manga-TX'
|
extName = 'Manga Empress'
|
||||||
extClass = '.Mangatxunoriginal'
|
extClass = '.MangaEmpress'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manga-tx.com'
|
baseUrl = 'https://mangaempress.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 33 KiB |
|
@ -0,0 +1,12 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.mangatxunoriginal
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class MangaEmpress : Madara(
|
||||||
|
"Manga Empress",
|
||||||
|
"https://mangaempress.com",
|
||||||
|
"en",
|
||||||
|
) {
|
||||||
|
// formally Manga-TX
|
||||||
|
override val id = 3683271326486389724
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangatxunoriginal
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Mangatxunoriginal : Madara(
|
|
||||||
"Manga-TX",
|
|
||||||
"https://manga-tx.com",
|
|
||||||
"en",
|
|
||||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
|
||||||
)
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Dat-Gar Scan'
|
||||||
|
extClass = '.DatGarScanlation'
|
||||||
|
themePkg = 'zeistmanga'
|
||||||
|
baseUrl = 'https://datgarscanlation.blogspot.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,18 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.es.datgarscanlation
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.zeistmanga.ZeistManga
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
|
||||||
|
class DatGarScanlation : ZeistManga(
|
||||||
|
"Dat-Gar Scan",
|
||||||
|
"https://datgarscanlation.blogspot.com",
|
||||||
|
"es",
|
||||||
|
) {
|
||||||
|
override val useNewChapterFeed = true
|
||||||
|
override val hasFilters = true
|
||||||
|
override val hasLanguageFilter = false
|
||||||
|
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'DeManhuas'
|
||||||
|
extClass = '.DeManhuas'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://demanhuas.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.es.demanhuas
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class DeManhuas : Madara(
|
||||||
|
"DeManhuas",
|
||||||
|
"https://demanhuas.com",
|
||||||
|
"es",
|
||||||
|
SimpleDateFormat("MMMM d, yyyy", Locale("es")),
|
||||||
|
) {
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val mangaSubString = "sm"
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
}
|
|
@ -3,7 +3,11 @@ ext {
|
||||||
extClass = '.EmperorScan'
|
extClass = '.EmperorScan'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://emperorscan.com'
|
baseUrl = 'https://emperorscan.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:randomua'))
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,47 @@
|
||||||
package eu.kanade.tachiyomi.extension.es.emperorscan
|
package eu.kanade.tachiyomi.extension.es.emperorscan
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class EmperorScan : Madara(
|
class EmperorScan :
|
||||||
|
Madara(
|
||||||
"Emperor Scan",
|
"Emperor Scan",
|
||||||
"https://emperorscan.com",
|
"https://emperorscan.com",
|
||||||
"es",
|
"es",
|
||||||
SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
|
||||||
) {
|
),
|
||||||
override val mangaDetailsSelectorDescription = "div.sinopsis div.contenedor"
|
ConfigurableSource {
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences =
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
|
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.setRandomUserAgent(
|
||||||
|
preferences.getPrefUAType(),
|
||||||
|
preferences.getPrefCustomUA(),
|
||||||
|
)
|
||||||
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val mangaDetailsSelectorDescription = "div.tab-summary div.sinopsis p"
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
addRandomUAPreferenceToScreen(screen)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Ikigai Mangas'
|
extName = 'Ikigai Mangas'
|
||||||
extClass = '.IkigaiMangas'
|
extClass = '.IkigaiMangas'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ class IkigaiMangas : HttpSource() {
|
||||||
|
|
||||||
override val baseUrl: String = "https://ikigaimangas.com"
|
override val baseUrl: String = "https://ikigaimangas.com"
|
||||||
private val apiBaseUrl: String = "https://panel.ikigaimangas.com"
|
private val apiBaseUrl: String = "https://panel.ikigaimangas.com"
|
||||||
private val pageViewerUrl: String = "https://ikigaitoon.com"
|
|
||||||
|
|
||||||
override val lang: String = "es"
|
override val lang: String = "es"
|
||||||
override val name: String = "Ikigai Mangas"
|
override val name: String = "Ikigai Mangas"
|
||||||
|
@ -114,21 +113,33 @@ class IkigaiMangas : HttpSource() {
|
||||||
return result.series.toSMangaDetails()
|
return result.series.toSMangaDetails()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String = pageViewerUrl + chapter.url
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val id = manga.url.substringAfterLast("#")
|
val slug = manga.url.substringAfter("/series/comic-").substringBefore("#")
|
||||||
return GET("$apiBaseUrl/api/swf/series/$id/chapter-list")
|
return GET("$apiBaseUrl/api/swf/series/$slug/chapters?page=1", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
|
val slug = response.request.url.toString()
|
||||||
return result.data.map { it.toSChapter(dateFormat) }.reversed()
|
.substringAfter("/series/")
|
||||||
|
.substringBefore("/chapters")
|
||||||
|
var result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
|
||||||
|
val mangas = mutableListOf<SChapter>()
|
||||||
|
mangas.addAll(result.data.map { it.toSChapter(dateFormat) })
|
||||||
|
var page = 2
|
||||||
|
while (result.meta.hasNextPage()) {
|
||||||
|
val newResponse = client.newCall(GET("$apiBaseUrl/api/swf/series/$slug/chapters?page=$page", headers)).execute()
|
||||||
|
result = json.decodeFromString<PayloadChaptersDto>(newResponse.body.string())
|
||||||
|
mangas.addAll(result.data.map { it.toSChapter(dateFormat) })
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
return mangas
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
val id = chapter.url.substringAfter("/capitulo/")
|
val id = chapter.url.substringAfter("/capitulo/")
|
||||||
return GET("$apiBaseUrl/api/swf/chapters/$id")
|
return GET("$apiBaseUrl/api/swf/chapters/$id", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
|
|
@ -80,7 +80,8 @@ class PayloadSeriesDetailsDto(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class PayloadChaptersDto(
|
class PayloadChaptersDto(
|
||||||
var data: List<ChapterDto>,
|
val data: List<ChapterDto>,
|
||||||
|
val meta: ChapterMetaDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -100,6 +101,14 @@ class ChapterDto(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterMetaDto(
|
||||||
|
@SerialName("current_page") private val currentPage: Int,
|
||||||
|
@SerialName("last_page") private val lastPage: Int,
|
||||||
|
) {
|
||||||
|
fun hasNextPage() = currentPage < lastPage
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class PayloadPagesDto(
|
class PayloadPagesDto(
|
||||||
val chapter: PageDto,
|
val chapter: PageDto,
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Kumanga'
|
extName = 'Kumanga'
|
||||||
extClass = '.Kumanga'
|
extClass = '.Kumanga'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:randomua'))
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package eu.kanade.tachiyomi.extension.es.kumanga
|
package eu.kanade.tachiyomi.extension.es.kumanga
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
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.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.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
|
||||||
|
@ -21,12 +29,15 @@ import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class Kumanga : HttpSource() {
|
class Kumanga : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name = "Kumanga"
|
override val name = "Kumanga"
|
||||||
|
|
||||||
|
@ -42,6 +53,9 @@ class Kumanga : HttpSource() {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences =
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||||
|
@ -55,6 +69,10 @@ class Kumanga : HttpSource() {
|
||||||
.add("Upgrade-Insecure-Requests", "1")
|
.add("Upgrade-Insecure-Requests", "1")
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.setRandomUserAgent(
|
||||||
|
preferences.getPrefUAType(),
|
||||||
|
preferences.getPrefCustomUA(),
|
||||||
|
)
|
||||||
.rateLimit(1)
|
.rateLimit(1)
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
|
@ -139,21 +157,28 @@ class Kumanga : HttpSource() {
|
||||||
var document = response.asJsoup()
|
var document = response.asJsoup()
|
||||||
var location = document.location()
|
var location = document.location()
|
||||||
val params = document.select("script:containsData(totCntnts)").toString()
|
val params = document.select("script:containsData(totCntnts)").toString()
|
||||||
|
val pagesVar = params.substringAfter("totCntnts").substringAfter("=").substringBefore(";").trim()
|
||||||
val mangaId = params.substringAfter("mid=").substringBefore(";")
|
val chaptersNumber = params.substringAfter(pagesVar).substringAfter("=").substringBefore(";").toIntOrNull()
|
||||||
val mangaSlug = params.substringAfter("slg='").substringBefore("';")
|
val mangaId = params.substringAfter("mid").substringAfter("=").substringBefore(";").trim()
|
||||||
|
val mangaSlug = params.substringAfter("slg").substringAfter("=").substringBefore(";").trim().removeSurrounding("'")
|
||||||
var hasNextPage = document.select("ul.pagination li.next:not(.disabled)").isNotEmpty()
|
if (chaptersNumber != null) {
|
||||||
|
val numberOfPages = ((chaptersNumber - 10) / 10.toDouble() + 0.4).roundToInt()
|
||||||
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
|
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
|
||||||
var page = 2
|
var page = 2
|
||||||
while (hasNextPage) {
|
while (page <= numberOfPages) {
|
||||||
val pageHeaders = headersBuilder().set("Referer", location).build()
|
val pageHeaders = headersBuilder().set("Referer", location).build()
|
||||||
document = client.newCall(GET(baseUrl + getMangaUrl(mangaId, mangaSlug, page), pageHeaders)).execute().asJsoup()
|
document = client.newCall(
|
||||||
|
GET(
|
||||||
|
baseUrl + getMangaUrl(mangaId, mangaSlug, page),
|
||||||
|
pageHeaders,
|
||||||
|
),
|
||||||
|
).execute().asJsoup()
|
||||||
location = document.location()
|
location = document.location()
|
||||||
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
|
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
|
||||||
page++
|
page++
|
||||||
hasNextPage = document.select("ul.pagination li.next:not(.disabled)").isNotEmpty()
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("No fue posible obtener los capítulos")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +264,10 @@ class Kumanga : HttpSource() {
|
||||||
GenreList(getGenreList()),
|
GenreList(getGenreList()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
addRandomUAPreferenceToScreen(screen)
|
||||||
|
}
|
||||||
|
|
||||||
private class Type(name: String, val id: String) : Filter.CheckBox(name)
|
private class Type(name: String, val id: String) : Filter.CheckBox(name)
|
||||||
private class TypeList(types: List<Type>) : Filter.Group<Type>("Filtrar por tipos", types)
|
private class TypeList(types: List<Type>) : Filter.Group<Type>("Filtrar por tipos", types)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Territorio Lealtad'
|
||||||
|
extClass = '.TerritorioLealtad'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://territorioleal.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.es.territoriolealtad
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class TerritorioLealtad : Madara(
|
||||||
|
"Territorio Lealtad",
|
||||||
|
"https://territorioleal.com",
|
||||||
|
"es",
|
||||||
|
SimpleDateFormat("dd 'de' MMMM 'de' yyyy", Locale("es")),
|
||||||
|
) {
|
||||||
|
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
|
|
||||||
|
override val client: OkHttpClient = super.client.newBuilder()
|
||||||
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Banana-Scan'
|
extName = 'Harmony-Scan'
|
||||||
extClass = '.BananaScan'
|
extClass = '.HarmonyScan'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://banana-scan.com'
|
baseUrl = 'https://harmony-scan.fr'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 1.5 KiB |