Skip to content

Commit 745f636

Browse files
author
yemen
committed
feat(ar): add MangaCloud source
1 parent ef4fdea commit 745f636

9 files changed

Lines changed: 379 additions & 0 deletions

File tree

src/ar/mangacloud/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
alias(kei.plugins.extension)
3+
}
4+
5+
keiyoushi {
6+
name = "MangaCloud"
7+
versionCode = 1
8+
contentWarning = ContentWarning.SAFE
9+
libVersion = "1.4"
10+
11+
source {
12+
lang = "ar"
13+
baseUrl = "https://mangacloud.online"
14+
id = 4294967295
15+
}
16+
}
5.92 KB
Loading
3.02 KB
Loading
9.58 KB
Loading
19.9 KB
Loading
35.8 KB
Loading
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package eu.kanade.tachiyomi.extension.ar.mangacloud
2+
3+
import eu.kanade.tachiyomi.source.model.Page
4+
import eu.kanade.tachiyomi.source.model.SChapter
5+
import eu.kanade.tachiyomi.source.model.SManga
6+
import keiyoushi.utils.jsonInstance
7+
import kotlinx.serialization.json.JsonObject
8+
import kotlinx.serialization.json.contentOrNull
9+
import kotlinx.serialization.json.jsonArray
10+
import kotlinx.serialization.json.jsonObject
11+
import kotlinx.serialization.json.jsonPrimitive
12+
import okhttp3.Response
13+
14+
data class MangaData(
15+
val smanga: SManga,
16+
val altTitles: List<String> = emptyList(),
17+
val genres: List<String> = emptyList(),
18+
val statusString: String = "",
19+
)
20+
21+
data class MangaListResult(
22+
val mangas: List<MangaData>,
23+
val nextPageToken: String?,
24+
)
25+
26+
object FirestoreParser {
27+
28+
fun parseList(response: Response): MangaListResult {
29+
val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject
30+
val documents = json["documents"]?.jsonArray ?: return MangaListResult(emptyList(), null)
31+
val mangas = documents.mapNotNull { doc ->
32+
val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null
33+
val mangaId = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: return@mapNotNull null
34+
val altTitles = fields.stringArray("alternativeTitles")
35+
val genres = fields.stringArray("genres")
36+
val statusStr = fields.string("status")
37+
MangaData(
38+
smanga = SManga.create().apply {
39+
url = "/manga/$mangaId"
40+
title = fields.string("title")
41+
thumbnail_url = fields.string("coverImage")
42+
description = fields.string("description")
43+
author = fields.string("author")
44+
artist = fields.string("artist")
45+
genre = genres.joinToString(", ")
46+
status = when (statusStr) {
47+
"مستمر" -> SManga.ONGOING
48+
"مكتمل" -> SManga.COMPLETED
49+
"متروك", "متروكة" -> SManga.ON_HIATUS
50+
"معلق" -> SManga.ON_HIATUS
51+
else -> SManga.UNKNOWN
52+
}
53+
},
54+
altTitles = altTitles,
55+
genres = genres,
56+
statusString = statusStr,
57+
)
58+
}
59+
val nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull
60+
return MangaListResult(mangas, nextPageToken)
61+
}
62+
63+
fun parseDetails(response: Response): SManga {
64+
val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject
65+
val fields = json["fields"]?.jsonObject ?: return SManga.create()
66+
val mangaId = json["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: ""
67+
return SManga.create().apply {
68+
url = "/manga/$mangaId"
69+
title = fields.string("title")
70+
thumbnail_url = fields.string("coverImage")
71+
description = fields.string("description")
72+
author = fields.string("author")
73+
artist = fields.string("artist")
74+
genre = fields.stringArray("genres").joinToString(", ")
75+
status = when (fields.string("status")) {
76+
"مستمر" -> SManga.ONGOING
77+
"مكتمل" -> SManga.COMPLETED
78+
"متروك" -> SManga.ON_HIATUS
79+
else -> SManga.UNKNOWN
80+
}
81+
}
82+
}
83+
84+
fun parseChapters(response: Response): List<SChapter> {
85+
val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject
86+
val documents = json["documents"]?.jsonArray ?: return emptyList()
87+
return documents.mapNotNull { doc ->
88+
val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null
89+
val docName = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null
90+
val chapterNum = docName.substringAfterLast("/")
91+
val num = chapterNum.toIntOrNull() ?: 0
92+
SChapter.create().apply {
93+
url = docName.substringAfterLast("cloudmangas/")
94+
name = "الفصل $num"
95+
chapter_number = num.toFloat()
96+
}
97+
}.sortedByDescending { it.chapter_number }
98+
}
99+
100+
fun parsePages(response: Response): List<Page> {
101+
val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject
102+
val fields = json["fields"]?.jsonObject ?: return emptyList()
103+
val pagesArray = fields["pages"]?.jsonObject?.get("arrayValue")?.jsonObject?.get("values")?.jsonArray ?: return emptyList()
104+
return pagesArray.mapIndexed { index, value ->
105+
Page(index, imageUrl = value.jsonObject["stringValue"]?.jsonPrimitive?.contentOrNull ?: "")
106+
}
107+
}
108+
}
109+
110+
private fun JsonObject.string(key: String): String = this[key]?.jsonObject?.get("stringValue")?.jsonPrimitive?.contentOrNull ?: ""
111+
112+
private fun JsonObject.stringArray(key: String): List<String> = this[key]?.jsonObject?.get("arrayValue")?.jsonObject?.get("values")?.jsonArray
113+
?.map { it.jsonObject["stringValue"]?.jsonPrimitive?.contentOrNull ?: "" }
114+
?: emptyList()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package eu.kanade.tachiyomi.extension.ar.mangacloud
2+
3+
import eu.kanade.tachiyomi.source.model.Filter
4+
5+
internal class SortFilter(name: String, private val pairs: Array<Pair<String, String>>) : Filter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
6+
val selected get() = pairs[state].second
7+
}
8+
9+
internal class GenreFilter(name: String, private val pairs: Array<Pair<String, String>>) : Filter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
10+
val selected get() = pairs[state].second
11+
}
12+
13+
internal class StatusFilter(name: String, private val pairs: Array<Pair<String, String>>) : Filter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
14+
val selected get() = pairs[state].second
15+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package eu.kanade.tachiyomi.extension.ar.mangacloud
2+
3+
import eu.kanade.tachiyomi.network.GET
4+
import eu.kanade.tachiyomi.source.model.FilterList
5+
import eu.kanade.tachiyomi.source.model.MangasPage
6+
import eu.kanade.tachiyomi.source.model.Page
7+
import eu.kanade.tachiyomi.source.model.SChapter
8+
import eu.kanade.tachiyomi.source.model.SManga
9+
import eu.kanade.tachiyomi.source.online.HttpSource
10+
import keiyoushi.annotation.Source
11+
import keiyoushi.utils.firstInstanceOrNull
12+
import okhttp3.HttpUrl.Companion.toHttpUrl
13+
import okhttp3.Request
14+
import okhttp3.Response
15+
16+
@Source
17+
abstract class MangaCloud : HttpSource() {
18+
19+
override val supportsLatest = true
20+
21+
override fun popularMangaRequest(page: Int): Request {
22+
val url = FIRESTORE_URL.toHttpUrl().newBuilder()
23+
.addQueryParameter("pageSize", PAGE_SIZE.toString())
24+
.addQueryParameter("key", API_KEY)
25+
.apply {
26+
val token = lastPageToken
27+
if (token != null) addQueryParameter("pageToken", token)
28+
}
29+
.build()
30+
return GET(url, headers)
31+
}
32+
33+
override fun popularMangaParse(response: Response): MangasPage {
34+
val json = FirestoreParser.parseList(response)
35+
lastPageToken = json.nextPageToken
36+
return MangasPage(json.mangas.map { it.smanga }, json.nextPageToken != null)
37+
}
38+
39+
override fun latestUpdatesRequest(page: Int): Request {
40+
val url = FIRESTORE_URL.toHttpUrl().newBuilder()
41+
.addQueryParameter("pageSize", PAGE_SIZE.toString())
42+
.addQueryParameter("key", API_KEY)
43+
.addQueryParameter("orderBy", "updatedAt desc")
44+
.build()
45+
return GET(url, headers)
46+
}
47+
48+
override fun latestUpdatesParse(response: Response): MangasPage {
49+
val json = FirestoreParser.parseList(response)
50+
return MangasPage(json.mangas.map { it.smanga }, false)
51+
}
52+
53+
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
54+
currentSearchQuery = query
55+
currentSortFilter = filters.firstInstanceOrNull<SortFilter>()?.selected.orEmpty()
56+
currentGenreFilter = filters.firstInstanceOrNull<GenreFilter>()?.selected.orEmpty()
57+
currentStatusFilter = filters.firstInstanceOrNull<StatusFilter>()?.selected.orEmpty()
58+
val url = FIRESTORE_URL.toHttpUrl().newBuilder()
59+
.addQueryParameter("pageSize", "300")
60+
.addQueryParameter("key", API_KEY)
61+
.apply {
62+
when (currentSortFilter) {
63+
"new" -> addQueryParameter("orderBy", "createdAt desc")
64+
"updated" -> addQueryParameter("orderBy", "updatedAt desc")
65+
}
66+
}
67+
.build()
68+
return GET(url, headers)
69+
}
70+
71+
override fun searchMangaParse(response: Response): MangasPage {
72+
val query = currentSearchQuery.trim()
73+
val genre = currentGenreFilter
74+
val status = currentStatusFilter
75+
val json = FirestoreParser.parseList(response)
76+
val filtered = json.mangas.filter { data ->
77+
val matchesQuery = if (query.isNotBlank()) {
78+
val q = query.lowercase()
79+
data.smanga.title.lowercase().contains(q) ||
80+
data.altTitles.any { it.lowercase().contains(q) } ||
81+
data.genres.any { it.lowercase().contains(q) } ||
82+
data.smanga.description?.lowercase()?.contains(q) == true
83+
} else {
84+
true
85+
}
86+
val matchesGenre = if (genre.isNotBlank()) {
87+
data.genres.any { it.lowercase().contains(genre.lowercase()) }
88+
} else {
89+
true
90+
}
91+
val matchesStatus = if (status.isNotBlank()) {
92+
data.statusString == status
93+
} else {
94+
true
95+
}
96+
matchesQuery && matchesGenre && matchesStatus
97+
}
98+
return MangasPage(filtered.map { it.smanga }, false)
99+
}
100+
101+
override fun mangaDetailsRequest(manga: SManga): Request {
102+
val mangaId = manga.url.substringAfterLast("/")
103+
val url = "$FIRESTORE_URL/$mangaId?key=$API_KEY".toHttpUrl()
104+
return GET(url, headers)
105+
}
106+
107+
override fun mangaDetailsParse(response: Response): SManga = FirestoreParser.parseDetails(response)
108+
109+
override fun chapterListRequest(manga: SManga): Request {
110+
val mangaId = manga.url.substringAfterLast("/")
111+
val url = "$FIRESTORE_URL/$mangaId/chapters?pageSize=1000&key=$API_KEY&orderBy=index".toHttpUrl()
112+
return GET(url, headers)
113+
}
114+
115+
override fun chapterListParse(response: Response): List<SChapter> = FirestoreParser.parseChapters(response)
116+
117+
override fun pageListRequest(chapter: SChapter): Request {
118+
val url = "${FIRESTORE_URL}/${chapter.url}?key=$API_KEY".toHttpUrl()
119+
return GET(url, headers)
120+
}
121+
122+
override fun pageListParse(response: Response): List<Page> = FirestoreParser.parsePages(response)
123+
124+
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
125+
126+
override fun getMangaUrl(manga: SManga): String {
127+
val mangaId = manga.url.substringAfterLast("/")
128+
return "$baseUrl/manga/$mangaId"
129+
}
130+
131+
override fun getChapterUrl(chapter: SChapter): String {
132+
val parts = chapter.url.split("/")
133+
if (parts.size >= 2) {
134+
return "$baseUrl/manga/${parts[0]}/${parts[1]}"
135+
}
136+
return "$baseUrl/manga/${chapter.url}"
137+
}
138+
139+
override fun getFilterList(): FilterList = FilterList(
140+
SortFilter("ترتيب حسب", sortingList),
141+
StatusFilter("الحالة", statusList),
142+
GenreFilter("التصنيف", genreList),
143+
)
144+
145+
private var lastPageToken: String? = null
146+
private var currentSearchQuery: String = ""
147+
private var currentSortFilter: String = ""
148+
private var currentGenreFilter: String = ""
149+
private var currentStatusFilter: String = ""
150+
151+
companion object {
152+
private const val API_KEY = "AIzaSyAvdmgz_r_d89Eo8JBs9vAjUAJR451bMYU"
153+
private const val FIRESTORE_URL = "https://firestore.googleapis.com/v1/projects/ninja-brew-crew-4d79c/databases/(default)/documents/cloudmangas"
154+
private const val PAGE_SIZE = 20
155+
156+
private val sortingList = arrayOf(
157+
Pair("الأشهر", "popular"),
158+
Pair("آخر التحديثات", "updated"),
159+
Pair("إضافات جديدة", "new"),
160+
)
161+
162+
private val statusList = arrayOf(
163+
Pair("الكل", ""),
164+
Pair("مستمر", "مستمر"),
165+
Pair("مكتمل", "مكتمل"),
166+
Pair("متروك", "متروك"),
167+
Pair("متروكة", "متروكة"),
168+
Pair("معلق", "معلق"),
169+
)
170+
171+
private val genreList = arrayOf(
172+
Pair("الكل", ""),
173+
Pair("أكشن", "أكشن"),
174+
Pair("إعادة احياء", "إعادة احياء"),
175+
Pair("إيسيكاي", "إيسيكاي"),
176+
Pair("aktions", "aktions"),
177+
Pair("الفنون القتالية", "الفنون القتالية"),
178+
Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"),
179+
Pair("اتنين", "اتنين"),
180+
Pair("اثاره", "اثاره"),
181+
Pair("اثاره حربيه", "اثاره حربيه"),
182+
Pair("ادمان", "ادمان"),
183+
Pair("ادمى", "ادمى"),
184+
Pair("ادهم", "ادهم"),
185+
Pair("ارهاب", "ارهاب"),
186+
Pair("استرجاع", "استرجاع"),
187+
Pair("استنساخ", "استنساخ"),
188+
Pair("اسلحة خارقة", "اسلحة خارقة"),
189+
Pair("اسطوره", "اسطوره"),
190+
Pair("اشتغال", "اشتغال"),
191+
Pair("اميرة", "اميرة"),
192+
Pair("انجي", "انجي"),
193+
Pair("ببجي", "ببجي"),
194+
Pair("بزنس", "بزنس"),
195+
Pair("بطل غير اعتيادي", "بطل غير اعتيادي"),
196+
Pair("تبديل عوالم", "تبديل عوالم"),
197+
Pair("تجسيد", "تجسيد"),
198+
Pair("تقمص شخصيات", "تقمص شخصيات"),
199+
Pair("جراي", "جراي"),
200+
Pair("جثث", "جثث"),
201+
Pair("حريم", "حريم"),
202+
Pair("حياة مدرسية", "حياة مدرسية"),
203+
Pair("خوارق", "خوارق"),
204+
Pair("خيال", "خيال"),
205+
Pair("خيال علمي", "خيال علمي"),
206+
Pair("دوار", "دوار"),
207+
Pair("دراما", "دراما"),
208+
Pair("رعب", "رعب"),
209+
Pair("رومانسي", "رومانسي"),
210+
Pair("زومبي", "زومبي"),
211+
Pair("سحر", "سحر"),
212+
Pair("شريحة من الحياة", "شريحة من الحياة"),
213+
Pair("شونين", "شونين"),
214+
Pair("شياطين", "شياطين"),
215+
Pair("عصابات", "عصابات"),
216+
Pair("عسكري", "عسكري"),
217+
Pair("غموض", "غموض"),
218+
Pair("فتى خارق", "فتى خارق"),
219+
Pair("فنتازيا", "فنتازيا"),
220+
Pair("فوتون", "فوتون"),
221+
Pair("فنون قتاليه", "فنون قتاليه"),
222+
Pair("قراصنة", "قراصنة"),
223+
Pair("كوميدي", "كوميدي"),
224+
Pair("كوميديا", "كوميديا"),
225+
Pair("مغامرة", "مغامرة"),
226+
Pair("موسيقى", "موسيقى"),
227+
Pair("نفسي", "نفسي"),
228+
Pair("نهاية العالم", "نهاية العالم"),
229+
Pair("وحوش", "وحوش"),
230+
Pair("ويبتون", "ويبتون"),
231+
Pair("ياباني", "ياباني"),
232+
)
233+
}
234+
}

0 commit comments

Comments
 (0)