Compare commits

..

28 Commits

Author SHA1 Message Date
AwkwardPeak7 5292a9ff0a Gmanga multisrc: Add Dilar & MangaTales (#1767)
CI / Prepare job (push) Successful in 4s Details
CI / Build individual modules (push) Successful in 3m14s Details
CI / Publish repo (push) Successful in 43s Details
* gmanga multisrc

* search payload and filters refactor

* ratelimit

* distinct

* dynamic filters

* dilar

* gmanga multisrc: latest

* gmanga multisrc: search & filter

* gmanga multisrc: chapters & pages

* small cleanup

* remove obsolete preferences

* small cleanup & arabic tl

deepl

* Dilar: filter paid chapters

* GManga: use unencrypted alt api for chapters

* abstract away sort of chapters and pages

* remove chapters logic from multisrc class since all three have different logic

* remove `this`
2024-03-12 19:55:31 +00:00
bapeey 0d04d70929 Emperor Scan: Add randomUA (#1803)
* Add randomUa and fix description

* newline

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-12 19:55:31 +00:00
devil6venom 8a8e4d2a8d Update domain of Mangafreak (#1809)
* Update domain of Mangafreak

* Update build.gradle
2024-03-12 19:55:31 +00:00
bapeey b1b8833e97 Manga Demon: Update domain again (#1805)
* Update domain

* I trust
2024-03-12 19:55:31 +00:00
bapeey 696a40e725 Add TerritorioLealtad (#1804) 2024-03-12 19:55:31 +00:00
bapeey 8be756b8bc Add DeManhuas (#1802) 2024-03-12 19:55:31 +00:00
bapeey a3039453b0 Kumanga: Fix chapter list again and add randomUa (#1801)
I'll remove this source in the future
2024-03-12 19:55:31 +00:00
Ashborn c445ca0eb4 Nexo Scans: Update domain (#1787) (#1788) 2024-03-12 19:55:31 +00:00
Cuong M. Tran 66dd223155 New source: Hentai Slayer (#1783)
* New source: Hentai Slayer

* remove redundant genre & current time
2024-03-12 19:55:31 +00:00
AwkwardPeak7 33bfee0f2a ReYume: fix page list (#1779) 2024-03-12 19:55:31 +00:00
AwkwardPeak7 1d7c252a48 Komik AV -> Apkomik: change domain (#1778) 2024-03-12 19:55:31 +00:00
bapeey dcf9230a21 IkigaiMangas: Update chapterlist endpoint (#1771)
* Smh

* Update chapter list endpoint (paginated T-T)
2024-03-12 19:55:31 +00:00
AwkwardPeak7 2f4f7001ea Fix NPEs in some sources (#1773)
* Siren Komik: fix NPE

* West Manga: fix NPE
2024-03-12 19:55:31 +00:00
GoldenRover e2e0d9b034 I Roved Out: Fix downloads not finishing (#1758)
* Remove unimplemented override method

* Update extVersionCode
2024-03-12 19:55:31 +00:00
AwkwardPeak7 a2e3223685 Hentairead: fix results (#1739) 2024-03-12 19:55:31 +00:00
Chopper 8ec772ebbe Add ToshiWaYume (#1760) 2024-03-12 19:55:31 +00:00
Karuto 085efa9fa4 Change domain for nettruyen (#1716)
* change domain for nettruyen

* rechanging to original domain

* Revert "change domain for nettruyen"

This reverts commit 5642a0e48ce77e0558a02b2b966dc00199880bb0.

* modified:   ../../../../../../../build.gradle

* Revert "rechanging to original domain"

This reverts commit ad9a7b0250eaf50efa3f217d62bb2dfae5b0073e.

	modified:   NetTruyen.kt
	modified:   ../../../../../../../build.gradle

* last commit

* remove head and last commit p2

* .

* gap

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-12 19:55:31 +00:00
KirinRaikage f5ec446f74 Banana-Scan: Rename to Harmony-Scan and migrate to Madara (#1754)
* Banana-Scan: Rename to Harmony-Scan and migrate to Madara

* Add Banana Scan ID
2024-03-12 19:55:31 +00:00
AwkwardPeak7 9a7187bf36 MirrorDesu: change domain and decrypt pagelist (#1738) 2024-03-12 19:55:31 +00:00
AwkwardPeak7 20db7af324 rename Manga-TX to MangaEmpress (#1736)
Manga-TX -> MangaEmpress
2024-03-12 19:55:31 +00:00
AwkwardPeak7 635f732acf MangaWT: move to Madara (#1735) 2024-03-12 19:55:31 +00:00
AwkwardPeak7 5ff2906e28 RuyaManga: fix no results found (#1733)
* RuyaManga: fix no results found

* bump
2024-03-12 19:55:31 +00:00
AwkwardPeak7 1abb2f0fa6 DiamondFansub: fix selectors and directory (#1732) 2024-03-12 19:55:31 +00:00
AwkwardPeak7 1cd1935eeb Madara: fix redirects on manga from deeplink (#1731) 2024-03-12 19:55:31 +00:00
AwkwardPeak7 757b067214 MangaDistrict: fix next page (#1730)
* MangaDistrict: fix next page

* mangasubdirectory
2024-03-12 19:55:31 +00:00
Luqman f42f301af4 MangaThemesia: Status check (#1724)
use "Contains" like before
2024-03-12 19:55:31 +00:00
bapeey 3a76ddaf34 Add Dat-Gar Scan (#1723)
* Add DatGarScanlation

* Change name
2024-03-12 19:55:31 +00:00
mohamedotaku 5a1fdc542c add source MangaTak "ar" (#1703)
* add source MangaTak "ar"

* update res

* update res
2024-03-12 19:55:31 +00:00
128 changed files with 1782 additions and 941 deletions

View File

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

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.ar.gmanga
package eu.kanade.tachiyomi.multisrc.gmanga
import android.util.Base64
import java.security.MessageDigest
@ -14,7 +14,7 @@ fun decrypt(responseData: String): String {
}
private fun String.hexStringToByteArray(): ByteArray {
val len = this.length
val len = length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
@ -30,8 +30,8 @@ private fun String.hexStringToByteArray(): ByteArray {
private fun String.sha256(): String {
return MessageDigest
.getInstance("SHA-256")
.digest(this.toByteArray())
.fold("", { str, it -> str + "%02x".format(it) })
.digest(toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
}
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))
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))
}

View File

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

View File

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

View File

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

View File

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

View File

@ -237,7 +237,7 @@ abstract class Madara(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
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))
.asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply {

View File

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

View File

@ -272,29 +272,30 @@ abstract class MangaThemesia(
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
}
open fun String?.parseStatus(): Int {
if (this == null) return SManga.UNKNOWN
open fun String?.parseStatus(): Int = when {
this == null -> SManga.UNKNOWN
return when (this.lowercase().trim()) {
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
"en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing",
"devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision",
"en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando", "连载中", "devam etmekte", "連載中",
-> SManga.ONGOING
listOf(
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours", "en cours de publication",
"đang tiến hành", "em lançamento", "онгоінг", "publishing", "devam ediyor", "em andamento",
"in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo",
"emision", "en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando",
"连载中", "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",
"مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot", "bitti", "tamat", "completado", "concluído", "完結",
"concluido", "已完结", "bitmiş",
-> SManga.COMPLETED
listOf(
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành",
"hoàn thành", "مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot",
"bitti", "tamat", "completado", "concluído", "完結", "concluido", "已完结", "bitmiş",
).any { this.contains(it, ignoreCase = true) } -> SManga.COMPLETED
"canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné",
-> SManga.CANCELLED
listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné")
.any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
"hiatus", "on hold", "pausado", "en espera", "en pause", "en attente",
-> SManga.ON_HIATUS
listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente")
.any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
else -> SManga.UNKNOWN
}
// Chapter list

View File

@ -391,6 +391,7 @@ abstract class ZeistManga(
protected open val statusOnGoingList = listOf(
"ongoing",
"en curso",
"en emisión",
"ativo",
"lançando",
"مستمر",

View File

@ -0,0 +1,8 @@
ext {
extName = 'Dilar'
extClass = '.Dilar'
themePkg = 'gmanga'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

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

View File

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

View File

@ -1,7 +1,8 @@
ext {
extName = 'GMANGA'
extClass = '.Gmanga'
extVersionCode = 13
themePkg = 'gmanga'
overrideVersionCode = 13
}
apply from: "$rootDir/common.gradle"

View File

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

View File

@ -1,315 +1,90 @@
package eu.kanade.tachiyomi.extension.ar.gmanga
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_ALL
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 android.app.Application
import eu.kanade.tachiyomi.multisrc.gmanga.BrowseManga
import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
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.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.float
import kotlinx.serialization.json.jsonArray
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.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Gmanga : ConfigurableSource, HttpSource() {
private val domain: String = "gmanga.org"
override val baseUrl: String = "https://$domain"
override val lang: String = "ar"
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()
class Gmanga : Gmanga(
"GMANGA",
"https://gmanga.org",
"ar",
"https://media.gmanga.me",
) {
override val client = super.client.newBuilder()
.rateLimit(4)
.build()
private val parsedDatePattern: SimpleDateFormat = SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss ZZZ zzz",
Locale.ENGLISH,
)
private val formattedDatePattern: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", USER_AGENT)
init {
// remove obsolete preferences
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 setupPreferenceScreen(screen: PreferenceScreen) =
preferences.setupPreferenceScreen(screen)
override fun chapterListRequest(manga: SManga): Request {
val mangaId = manga.url.substringAfterLast("/")
return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val data = decryptResponse(response)
val table = json.decodeFromJsonElement<TableDto>(data)
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()
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])
}
return releases.map { release ->
SChapter.create().apply {
val chapter = chapterList.chapters.first { it.id == release.chapterizationId }
val team = chapterList.teams.firstOrNull { it.id == release.teamId }
val entries = manags.map { it.toSManga(::createThumbnail) }
.distinctBy { it.url }
url = "/r/${release.id}"
chapter_number = chapter.chapter
date_upload = release.timestamp * 1000
return MangasPage(
entries,
hasNextPage = (manags.size >= 30),
)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservable() // sites returns false 302 code
.map(::chapterListParse)
}
override fun chaptersRequest(manga: SManga): Request {
val mangaId = manga.url.substringAfterLast("/")
return GET("https://api2.gmanga.me/api/mangas/$mangaId/releases", headers)
}
override fun chaptersParse(response: Response): List<SChapter> {
val chapterList = response.parseAs<ChapterListResponse>()
return chapterList.releases.map {
SChapter.create().apply {
val chapter = chapterList.chapterizations.first { chap -> chap.id == it.chapId }
val team = chapterList.teams.firstOrNull { team -> team.id == it.teamId }
url = "/r/${it.id}"
chapter_number = it.chapter.float
date_upload = it.timestamp * 1000
scanlator = team?.name
val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
ext {
extName = 'Hentai Slayer'
extClass = '.HentaiSlayer'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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

View File

@ -0,0 +1,9 @@
ext {
extName = 'MangaTak'
extClass = '.MangaTak'
themePkg = 'mangathemesia'
baseUrl = 'https://mangatak.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

@ -0,0 +1,8 @@
ext {
extName = 'Manga Tales'
extClass = '.MangaTales'
themePkg = 'gmanga'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

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

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Hentairead'
themePkg = 'madara'
baseUrl = 'https://hentairead.com'
overrideVersionCode = 3
overrideVersionCode = 4
isNsfw = true
}

View File

@ -2,10 +2,16 @@ package eu.kanade.tachiyomi.extension.en.hentairead
import android.net.Uri
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.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 rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
@ -13,6 +19,22 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
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 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"
@ -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 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> {
launchIO { countViews(document) }
@ -37,12 +66,14 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
}
}
override fun chapterListParse(response: Response): List<SChapter> {
return listOf(
SChapter.create().apply {
name = "Chapter"
setUrlWithoutDomain(response.request.url.encodedPath)
},
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
name = "Chapter"
url = manga.url
},
),
)
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'I Roved Out'
extClass = '.IRovedOut'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View File

@ -90,8 +90,6 @@ class IRovedOut : HttpSource() {
return Observable.just(pages)
}
override fun pageListRequest(chapter: SChapter): Request = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
override fun fetchPopularManga(page: Int): Observable<MangasPage> {

View File

@ -1,7 +1,7 @@
ext {
extName = 'Manga Demon'
extClass = '.MangaDemon'
extVersionCode = 9
extVersionCode = 10
isNsfw = false
}

View File

@ -28,7 +28,7 @@ class MangaDemon : ParsedHttpSource() {
override val lang = "en"
override val supportsLatest = true
override val name = "Manga Demon"
override val baseUrl = "https://demontoon.com"
override val baseUrl = "https://demonreader.org"
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
@ -195,7 +195,7 @@ class MangaDemon : ParsedHttpSource() {
return SManga.create().apply {
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())
genre = infoElement.select("a.property-item").joinToString { it.text() }
description = infoElement.select("p.description").text()

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaDistrict'
themePkg = 'madara'
baseUrl = 'https://mangadistrict.com'
overrideVersionCode = 3
overrideVersionCode = 4
isNsfw = true
}

View File

@ -23,11 +23,13 @@ class MangaDistrict :
),
ConfigurableSource {
override val mangaSubString = "read-scan"
private val preferences: SharedPreferences by lazy {
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("\\(.*\\)")

View File

@ -1,7 +1,7 @@
ext {
extName = 'Mangafreak'
extClass = '.Mangafreak'
extVersionCode = 9
extVersionCode = 10
}

View File

@ -22,7 +22,7 @@ class Mangafreak : ParsedHttpSource() {
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

View File

@ -1,9 +1,9 @@
ext {
extName = 'Manga-TX'
extClass = '.Mangatxunoriginal'
extName = 'Manga Empress'
extClass = '.MangaEmpress'
themePkg = 'madara'
baseUrl = 'https://manga-tx.com'
overrideVersionCode = 0
baseUrl = 'https://mangaempress.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

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

View File

@ -0,0 +1,9 @@
ext {
extName = 'DeManhuas'
extClass = '.DeManhuas'
themePkg = 'madara'
baseUrl = 'https://demanhuas.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

@ -3,7 +3,11 @@ ext {
extClass = '.EmperorScan'
themePkg = 'madara'
baseUrl = 'https://emperorscan.com'
overrideVersionCode = 1
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:randomua'))
}

View File

@ -1,14 +1,47 @@
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.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.util.Locale
class EmperorScan : Madara(
"Emperor Scan",
"https://emperorscan.com",
"es",
SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
) {
override val mangaDetailsSelectorDescription = "div.sinopsis div.contenedor"
class EmperorScan :
Madara(
"Emperor Scan",
"https://emperorscan.com",
"es",
SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
),
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)
}
}

View File

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

View File

@ -24,7 +24,6 @@ class IkigaiMangas : HttpSource() {
override val baseUrl: String = "https://ikigaimangas.com"
private val apiBaseUrl: String = "https://panel.ikigaimangas.com"
private val pageViewerUrl: String = "https://ikigaitoon.com"
override val lang: String = "es"
override val name: String = "Ikigai Mangas"
@ -114,21 +113,33 @@ class IkigaiMangas : HttpSource() {
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 {
val id = manga.url.substringAfterLast("#")
return GET("$apiBaseUrl/api/swf/series/$id/chapter-list")
val slug = manga.url.substringAfter("/series/comic-").substringBefore("#")
return GET("$apiBaseUrl/api/swf/series/$slug/chapters?page=1", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
return result.data.map { it.toSChapter(dateFormat) }.reversed()
val slug = response.request.url.toString()
.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 {
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> {

View File

@ -80,7 +80,8 @@ class PayloadSeriesDetailsDto(
@Serializable
class PayloadChaptersDto(
var data: List<ChapterDto>,
val data: List<ChapterDto>,
val meta: ChapterMetaDto,
)
@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
class PayloadPagesDto(
val chapter: PageDto,

View File

@ -1,7 +1,11 @@
ext {
extName = 'Kumanga'
extClass = '.Kumanga'
extVersionCode = 10
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:randomua'))
}

View File

@ -1,9 +1,17 @@
package eu.kanade.tachiyomi.extension.es.kumanga
import android.app.Application
import android.content.SharedPreferences
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.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -21,12 +29,15 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.roundToInt
class Kumanga : HttpSource() {
class Kumanga : HttpSource(), ConfigurableSource {
override val name = "Kumanga"
@ -42,6 +53,9 @@ class Kumanga : HttpSource() {
private val json: Json by injectLazy()
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.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")
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
)
.rateLimit(1)
.addInterceptor { chain ->
val request = chain.request()
@ -139,21 +157,28 @@ class Kumanga : HttpSource() {
var document = response.asJsoup()
var location = document.location()
val params = document.select("script:containsData(totCntnts)").toString()
val mangaId = params.substringAfter("mid=").substringBefore(";")
val mangaSlug = params.substringAfter("slg='").substringBefore("';")
var hasNextPage = document.select("ul.pagination li.next:not(.disabled)").isNotEmpty()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
var page = 2
while (hasNextPage) {
val pageHeaders = headersBuilder().set("Referer", location).build()
document = client.newCall(GET(baseUrl + getMangaUrl(mangaId, mangaSlug, page), pageHeaders)).execute().asJsoup()
location = document.location()
val pagesVar = params.substringAfter("totCntnts").substringAfter("=").substringBefore(";").trim()
val chaptersNumber = params.substringAfter(pagesVar).substringAfter("=").substringBefore(";").toIntOrNull()
val mangaId = params.substringAfter("mid").substringAfter("=").substringBefore(";").trim()
val mangaSlug = params.substringAfter("slg").substringAfter("=").substringBefore(";").trim().removeSurrounding("'")
if (chaptersNumber != null) {
val numberOfPages = ((chaptersNumber - 10) / 10.toDouble() + 0.4).roundToInt()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
page++
hasNextPage = document.select("ul.pagination li.next:not(.disabled)").isNotEmpty()
var page = 2
while (page <= numberOfPages) {
val pageHeaders = headersBuilder().set("Referer", location).build()
document = client.newCall(
GET(
baseUrl + getMangaUrl(mangaId, mangaSlug, page),
pageHeaders,
),
).execute().asJsoup()
location = document.location()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
page++
}
} else {
throw Exception("No fue posible obtener los capítulos")
}
}
@ -239,6 +264,10 @@ class Kumanga : HttpSource() {
GenreList(getGenreList()),
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
private class Type(name: String, val id: String) : Filter.CheckBox(name)
private class TypeList(types: List<Type>) : Filter.Group<Type>("Filtrar por tipos", types)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

@ -1,9 +1,9 @@
ext {
extName = 'Banana-Scan'
extClass = '.BananaScan'
themePkg = 'mangathemesia'
baseUrl = 'https://banana-scan.com'
overrideVersionCode = 0
extName = 'Harmony-Scan'
extClass = '.HarmonyScan'
themePkg = 'madara'
baseUrl = 'https://harmony-scan.fr'
overrideVersionCode = 1
isNsfw = true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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