Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'GMANGA'
pkgNameSuffix = 'ar.gmanga'
extClass = '.Gmanga'
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -0,0 +1,315 @@
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 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.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.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
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()
.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)
}
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()
}
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 }
url = "/r/${release.id}"
chapter_number = chapter.chapter
date_upload = release.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("Not used")
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

@@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.extension.ar.gmanga
import android.util.Base64
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
fun decrypt(responseData: String): String {
val enc = responseData.split("|")
val secretKey = enc[3].sha256().hexStringToByteArray()
return enc[0].aesDecrypt(secretKey, enc[2])
}
private fun String.hexStringToByteArray(): ByteArray {
val len = this.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = (
(Character.digit(this[i], 16) shl 4) +
Character.digit(this[i + 1], 16)
).toByte()
i += 2
}
return data
}
private fun String.sha256(): String {
return MessageDigest
.getInstance("SHA-256")
.digest(this.toByteArray())
.fold("", { str, it -> str + "%02x".format(it) })
}
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
val sk = SecretKeySpec(secretKey, "AES")
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)
return String(c.doFinal(byteStr))
}

View File

@@ -0,0 +1,291 @@
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

@@ -0,0 +1,81 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,9 @@
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,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Manga.ae'
pkgNameSuffix = 'ar.mangaae'
extClass = '.MangaAe'
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,185 @@
package eu.kanade.tachiyomi.extension.ar.mangaae
import android.app.Application
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.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
class MangaAe : ParsedHttpSource(), ConfigurableSource {
override val name = "مانجا العرب"
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
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)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/page:$page", headers)
override fun popularMangaSelector() = "div.mangacontainer"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.selectFirst("img")?.run {
attr("data-pagespeed-lazy-src").ifEmpty { attr("src") }
}
element.selectFirst("div.mangacontainer a.manga")!!.run {
title = text()
setUrlWithoutDomain(absUrl("href"))
}
}
override fun popularMangaNextPageSelector() = "div.pagination a:last-child:not(.active)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesSelector() = "div.popular-manga-container"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.selectFirst("img")?.run {
attr("data-pagespeed-lazy-src").ifEmpty { attr("src") }
}
setUrlWithoutDomain(element.selectFirst("a:has(img)")!!.attr("href"))
title = element.selectFirst("a:last-child")!!.text()
}
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = buildString {
append("$baseUrl/manga/search:$query|page:$page")
filters.firstOrNull { it is OrderByFilter }
?.takeUnless { it.state == 0 }
?.also {
val filter = it as OrderByFilter
append("|order:${filter.toUriPart()}")
}
append("|arrange:minus")
}
return GET(url.toHttpUrl(), 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 {
val infoElement = document.selectFirst("div.indexcontainer")!!
// Essential info, a NPE may be understandable
with(infoElement) {
title = selectFirst("h1.EnglishName")!!.text().removeSurrounding("(", ")")
author = selectFirst("div.manga-details-author h4")?.text()
artist = author
thumbnail_url = selectFirst("img.manga-cover")?.attr("src")
}
// Additional info
infoElement.selectFirst("div.manga-details-extended")?.run {
status = parseStatus(selectFirst("td h4")?.text().orEmpty())
genre = select("a[href*=tag]").eachText().joinToString()
description = selectFirst("h4[style*=overflow-y]")?.text()
}
}
private fun parseStatus(status: String) = when {
status.contains("مستمرة") -> SManga.ONGOING
status.contains("مكتملة") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// ============================== Chapters ==============================
override fun chapterListSelector() = "ul.new-manga-chapters > li a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href").removeSuffix("/1/") + "/0/allpages")
name = "\u061C" + element.text() // Add unicode ARABIC LETTER MARK to ensure all titles are right to left
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("div#showchaptercontainer img").mapIndexed { index, item ->
Page(index, "", item.attr("src"))
}
}
override fun imageUrlParse(document: Document): String = throw Exception("Not used")
// ============================== Filters ===============================
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private class OrderByFilter : UriPartFilter(
"الترتيب حسب",
arrayOf(
Pair("اختيار", ""),
Pair("اسم المانجا", "english_name"),
Pair("تاريخ النشر", "release_date"),
Pair("عدد الفصول", "chapter_count"),
Pair("الحالة", "status"),
),
)
override fun getFilterList() = FilterList(OrderByFilter())
// ============================== Settings ==============================
companion object {
private const val RESTART_TACHIYOMI = ".لتطبيق الإعدادات الجديدة Tachiyomi أعد تشغيل"
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
private const val BASE_URL_PREF_DEFAULT = "https://manga.ae"
private const val BASE_URL_PREF = "overrideBaseUrl_v${BuildConfig.VERSION_CODE}"
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
setDefaultValue(BASE_URL_PREF_DEFAULT)
dialogTitle = BASE_URL_PREF_TITLE
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
}
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, BASE_URL_PREF_DEFAULT)!!
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'RE Manga'
pkgNameSuffix = 'ar.remanga'
extClass = '.REManga'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -0,0 +1,258 @@
package eu.kanade.tachiyomi.extension.ar.remanga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
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 java.text.SimpleDateFormat
import java.util.Locale
class REManga : ParsedHttpSource() {
override val name = "RE Manga"
override val baseUrl = "https://re-manga.com"
override val lang = "ar"
override val supportsLatest = true
// Popular
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=popular&status=&type=")
override fun popularMangaSelector() = "article.animpost"
override fun popularMangaFromElement(element: Element): SManga =
SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("abs:href"))
element.select("img").let {
thumbnail_url = it.attr("abs:src")
title = it.attr("title")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=update&status=&type=")
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga-list/?".toHttpUrl().newBuilder()
.addQueryParameter("title", query)
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("order", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
is GenreFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("genre[]", it.id) }
}
is YearFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("years[]", it.id) }
}
else -> {}
}
}
return GET(url.toString(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("div.infox").first()!!.let { info ->
title = info.select("h1").text()
}
description = document.select("div.desc > div > p").text()
genre = document.select("div.spe > span:contains(نوع), div.genre-info > a").joinToString { it.text() }
document.select("div.spe > span:contains(الحالة)").first()?.text()?.also { statusText ->
when {
statusText.contains("مستمر", true) -> status = SManga.ONGOING
else -> status = SManga.COMPLETED
}
}
}
}
// Chapters
override fun chapterListSelector() = ".lsteps li"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
val chNum = element.select(".eps > a").first()!!.text()
val chTitle = element.select(".lchx > a").first()!!.text()
name = when {
chTitle.startsWith("الفصل ") -> chTitle
else -> "الفصل $chNum - $chTitle"
}
element.select(".date").first()?.text()?.let { date ->
date_upload = DATE_FORMATTER.parse(date)?.time ?: 0L
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div.reader-area img").mapIndexed { i, img ->
Page(i, "", img.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters
override fun getFilterList() = FilterList(
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
TypeFilter(getTypeFilter()),
Filter.Separator(),
Filter.Header("exclusion not available for This source"),
GenreFilter(getGenreFilters()),
YearFilter(getYearFilters()),
)
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Sort by", vals)
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Type", vals)
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
class Genre(name: String, val id: String = name) : Filter.TriState(name)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
class Year(name: String, val id: String = name) : Filter.TriState(name)
private class YearFilter(years: List<Year>) : Filter.Group<Year>("Year", years)
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("title", "A-Z"),
Pair("titlereverse", "Z-A"),
Pair("update", "Latest Update"),
Pair("latest", "Latest Added"),
Pair("popular", "Popular"),
)
private fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Publishing", "مستمر"),
Pair("Finished", "تاريخ انتهي"),
)
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Manga", "Manga"),
Pair("Manhwa", "Manhwa"),
Pair("Manhua", "Manhua"),
)
private fun getGenreFilters(): List<Genre> = listOf(
Genre("Action", "action"),
Genre("Adventure", "adventure"),
Genre("Comedy", "comedy"),
Genre("Dementia", "dementia"),
Genre("Demons", "demons"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Josei", "josei"),
Genre("Magic", "magic"),
Genre("Martial Arts", "martial-arts"),
Genre("Military", "military"),
Genre("Mystery", "mystery"),
Genre("Parody", "parody"),
Genre("Psychological", "psychological"),
Genre("Romance", "romance"),
Genre("Samurai", "samurai"),
Genre("School", "school"),
Genre("Sci-Fi", "sci-fi"),
Genre("Seinen", "seinen"),
Genre("Shounen", "shounen"),
Genre("Slice of Life", "slice-of-life"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"),
Genre("Supernatural", "supernatural"),
Genre("Vampire", "vampire"),
)
private fun getYearFilters(): List<Year> = listOf(
Year("1970", "1970"),
Year("1986", "1986"),
Year("1989", "1989"),
Year("1995", "1995"),
Year("1997", "1997"),
Year("1998", "1998"),
Year("1999", "1999"),
Year("2000", "2000"),
Year("2002", "2002"),
Year("2003", "2003"),
Year("2004", "2004"),
Year("2005", "2005"),
Year("2006", "2006"),
Year("2007", "2007"),
Year("2008", "2008"),
Year("2009", "2009"),
Year("2010", "2010"),
Year("2011", "2011"),
Year("2012", "2012"),
Year("2013", "2013"),
Year("2014", "2014"),
Year("2016", "2016"),
Year("2017", "2017"),
Year("2018", "2018"),
Year("2019", "2019"),
Year("2020", "2020"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMM d, yyy", Locale("ar"))
}
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Shqqaa Manga'
pkgNameSuffix = 'ar.shqqaa'
extClass = '.Shqqaa'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.extension.ar.shqqaa
import eu.kanade.tachiyomi.network.GET
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.ParsedHttpSource
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class Shqqaa : ParsedHttpSource() {
override val name = "مانجا شقاع"
override val baseUrl = "https://www.shqqaa.com"
override val lang = "ar"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/manga", headers)
}
override fun popularMangaSelector() = "div.card"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.select("img").first()!!.attr("data-src")
element.select("a").first()!!.let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title").split(", ")[0]
}
return manga
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/manga/chapters/", headers)
}
override fun latestUpdatesSelector(): String = "div.row > div.col-xl-3"
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a").first()!!.let {
manga.setUrlWithoutDomain("${it.attr("href").substringBeforeLast('/')}/")
manga.title = element.select("small").first()!!.text().split(", ")[0]
}
manga.thumbnail_url = element.select("img").first()!!.attr("data-src")
return manga
}
override fun latestUpdatesNextPageSelector(): String? = null
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = fetchPopularManga(1)
.map { mp -> MangasPage(mp.mangas.filter { it.title.contains(query, ignoreCase = true) }, false) }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw Exception("Not used")
override fun searchMangaSelector(): String = throw Exception("Not Used")
override fun searchMangaFromElement(element: Element): SManga = throw Exception("Not Used")
override fun searchMangaNextPageSelector() = throw Exception("Not Used")
// Manga summary page
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div.col-sm-12")
val mangaInfo = infoElement[1]
val manga = SManga.create()
manga.title = mangaInfo.select("small.text-muted")[1].ownText().split(", ")[0]
manga.author = null
val status = mangaInfo.select("span.badge").first()!!.ownText()
manga.status = parseStatus(status)
manga.genre = null
manga.description = infoElement.first()!!.select(".text-muted").first()!!.ownText()
manga.thumbnail_url = mangaInfo.select("img").attr("data-src")
return manga
}
private fun parseStatus(status: String?) = when {
status == null -> SManga.UNKNOWN
status.contains("مستمر") -> SManga.ONGOING
status.contains("منتهي") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListSelector() = "a.m-1"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
element.select("a").let {
chapter.setUrlWithoutDomain(it.attr("href"))
chapter.name = it.text()
}
chapter.date_upload = 0
return chapter
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("div.img-manga img").forEach {
pages.add(Page(pages.size, "", it.attr("src")))
}
return pages
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
override fun getFilterList() = FilterList()
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

12
src/ar/teamx/build.gradle Normal file
View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Team X'
pkgNameSuffix = 'ar.teamx'
extClass = '.TeamX'
extVersionCode = 16
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,199 @@
package eu.kanade.tachiyomi.extension.ar.teamx
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class TeamX : ParsedHttpSource() {
override val name = "Team X"
override val baseUrl = "https://team1x12.com"
override val lang = "ar"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(10, 1, TimeUnit.SECONDS)
.build()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/series/" + if (page > 1) "?page=$page" else "", headers)
}
override fun popularMangaSelector() = "div.listupd div.bsx"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("a").attr("title")
setUrlWithoutDomain(element.select("a").first()!!.attr("href"))
thumbnail_url = element.select("img").let {
if (it.hasAttr("data-src")) {
it.attr("abs:data-src")
} else {
it.attr("abs:src")
}
}
}
}
override fun popularMangaNextPageSelector() = "a[rel=next]"
// Latest
private val titlesAdded = mutableSetOf<String>()
override fun latestUpdatesRequest(page: Int): Request {
if (page == 1) titlesAdded.clear()
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val unfilteredManga = document.select(latestUpdatesSelector())
val mangaList = unfilteredManga.map { element ->
latestUpdatesFromElement(element)
}.distinctBy {
it.title
}.filter {
!titlesAdded.contains(it.title)
}
titlesAdded.addAll(mangaList.map { it.title })
return MangasPage(mangaList, document.select(latestUpdatesNextPageSelector()).isNotEmpty())
}
override fun latestUpdatesSelector() = "div.last-chapter div.box"
override fun latestUpdatesFromElement(element: Element): SManga {
return SManga.create().apply {
val linkElement = element.select("div.info a")
title = linkElement.select("h3").text()
setUrlWithoutDomain(linkElement.first()!!.attr("href"))
thumbnail_url = element.select("div.imgu img").first()!!.absUrl("src")
}
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/ajax/search?keyword=$query", headers)
}
override fun searchMangaSelector() = "li.list-group-item"
override fun searchMangaFromElement(element: Element): SManga {
return SManga.create().apply {
val urlAndText = element.select("div.ms-2 a")
title = urlAndText.text()
setUrlWithoutDomain(urlAndText.first()!!.absUrl("href"))
thumbnail_url = element.select("a img").first()!!.absUrl("src")
}
}
// doesnt matter as there is no next page
override fun searchMangaNextPageSelector(): String? = null
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
title = document.select("div.author-info-title h1").text()
description = document.select("div.review-content").text()
if (description.isNullOrBlank()) {
description = document.select("div.review-content p").text()
}
genre = document.select("div.review-author-info a").joinToString { it.text() }
thumbnail_url = document.select("div.text-right img").first()!!.absUrl("src")
}
}
// Chapters
private fun chapterNextPageSelector() = popularMangaNextPageSelector()
override fun chapterListParse(response: Response): List<SChapter> {
val allElements = mutableListOf<Element>()
var document = response.asJsoup()
while (true) {
val pageChapters = document.select(chapterListSelector())
if (pageChapters.isEmpty()) {
break
}
allElements += pageChapters
val hasNextPage = document.select(chapterNextPageSelector()).isNotEmpty()
if (!hasNextPage) {
break
}
val nextUrl = document.select(chapterNextPageSelector()).attr("href")
document = client.newCall(GET(nextUrl, headers)).execute().asJsoup()
}
return allElements.map { chapterFromElement(it) }
}
private val chapterFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
override fun chapterListSelector() = "div.eplister ul a"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
val chpNum = element.select("div.epl-num").text()
val chpTitle = element.select("div.epl-title").text()
name = when (chpNum.isNullOrBlank()) {
true -> chpTitle
false -> "$chpNum - $chpTitle"
}
date_upload = parseChapterDate(element.select("div.epl-date").text())
setUrlWithoutDomain(element.attr("href"))
}
}
private fun parseChapterDate(date: String): Long {
return runCatching {
chapterFormat.parse(date)?.time
}.getOrNull() ?: 0
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div.image_list img[src]").mapIndexed { i, img ->
Page(i, "", img.absUrl("src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
}