diff --git a/src/ar/mtlarabic/build.gradle.kts b/src/ar/mtlarabic/build.gradle.kts new file mode 100644 index 000000000..883d888e7 --- /dev/null +++ b/src/ar/mtlarabic/build.gradle.kts @@ -0,0 +1,21 @@ +import io.github.keiyoushi.gradle.api.ContentWarning + +plugins { + alias(ns.plugins.extension) +} + +keiyoushi { + name = "MTL Arabic" + versionCode = 1 + contentWarning = ContentWarning.SAFE + + source { + lang = "ar" + baseUrl = "https://mtlarabic.com" + } + + deeplink { + host("mtlarabic.com") + path("/..*") + } +} diff --git a/src/ar/mtlarabic/res/mipmap-hdpi/ic_launcher.png b/src/ar/mtlarabic/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..1cbbd54a4 Binary files /dev/null and b/src/ar/mtlarabic/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ar/mtlarabic/res/mipmap-mdpi/ic_launcher.png b/src/ar/mtlarabic/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..d3dc6b3a4 Binary files /dev/null and b/src/ar/mtlarabic/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ar/mtlarabic/res/mipmap-xhdpi/ic_launcher.png b/src/ar/mtlarabic/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..880b00935 Binary files /dev/null and b/src/ar/mtlarabic/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ar/mtlarabic/res/mipmap-xxhdpi/ic_launcher.png b/src/ar/mtlarabic/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..24e27677c Binary files /dev/null and b/src/ar/mtlarabic/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ar/mtlarabic/res/mipmap-xxxhdpi/ic_launcher.png b/src/ar/mtlarabic/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..22f6ca91e Binary files /dev/null and b/src/ar/mtlarabic/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/Dto.kt b/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/Dto.kt new file mode 100644 index 000000000..4d6213414 --- /dev/null +++ b/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/Dto.kt @@ -0,0 +1,129 @@ +package eu.kanade.tachiyomi.novelextension.ar.mtlarabic + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.parseAs +import keiyoushi.utils.tryParse +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.Response +import kotlin.time.Instant + +/** [SManga.url] holds the numeric novel id behind this prefix. */ +const val ID_PREFIX = "id-" + +/** The only place the site serves real JSON; everything else is HTML with an island in it. */ +val islandJson = Json { ignoreUnknownKeys = true } + +@Serializable +class ListingResponse( + val items: List = emptyList(), + val pagination: ListingPagination = ListingPagination(), +) + +@Serializable +class ListingPagination( + val currentPage: Int = 1, + val totalPages: Int = 1, + val pageSize: Int = 12, +) + +@Serializable +class ListingItem( + val id: Int = 0, + val name: String = "", + val type: String = "", + val status: String = "", + /** A bare file name; the site serves covers from /images/novels/. */ + val image: String = "", +) { + fun toSManga(baseUrl: String) = SManga.create().apply { + // The numeric id is stored rather than the slug: slugs are Arabic and contain + // characters that have to be percent-encoded, and hand-encoding them is where this + // goes wrong. The id round-trips through any url and needs no encoding. + url = ID_PREFIX + id + title = name + thumbnail_url = coverUrl(baseUrl, image) + genre = type + status = mangaStatus(this@ListingItem.status) + } +} + +@Serializable +class NovelDetails( + val id: Int = 0, + val slug: String = "", + val name: String = "", + val originalName: String = "", + val description: String = "", + val type: String = "", + val status: String = "", + val image: String = "", + val totalChapters: Int = 0, +) + +@Serializable +class ChaptersResponse( + val chapters: List = emptyList(), + val pagination: ChapterPagination = ChapterPagination(), +) + +@Serializable +class ChapterPagination( + val totalChapters: Int = 0, + val totalPages: Int = 1, + val currentPage: Int = 1, +) + +@Serializable +class ChapterDto( + val number: Int = 0, + val title: String = "", + val approvalDate: String = "", +) { + fun toSChapter(novelSlug: String) = SChapter.create().apply { + url = "/$novelSlug/$number" + name = title.ifBlank { "الفصل $number" } + chapter_number = number.toFloat() + date_upload = Instant.tryParse(approvalDate) + } +} + +/** The reader's JSON island, which carries the chapter body as plain text. */ +@Serializable +class ChapterPage( + val content: String = "", +) + +fun coverUrl(baseUrl: String, fileName: String): String { + if (fileName.isEmpty()) return "" + if (fileName.startsWith("http")) return fileName + // The listing and the detail page both send a bare file name, never a path: the cover lives at + // /images/novels/. It has to be an absolute url -- a leading-slash path is not a valid + // thumbnail_url and renders as an empty cover. + return baseUrl + "/images/novels/$fileName" +} + +/** The site writes exactly these two status literals and nothing else. */ +fun mangaStatus(raw: String): Int = when (raw) { + "مستمرة" -> SManga.ONGOING + "مكتملة" -> SManga.COMPLETED + else -> SManga.UNKNOWN +} + +/** + * `/novels`, `/novel-details` and the reader all answer with a full HTML page whose payload is a + * JSON island in a `""", RegexOption.IGNORE_CASE) + +/** Pulls the first island out of an HTML page, for use as a `String.parseAs` transform. */ +fun extractIsland(html: String): String = ISLAND.find(html)?.groupValues?.get(1) + ?: throw IllegalStateException("No JSON island in the page") + +/** Parses a response body that is an HTML page with the JSON embedded in a script tag. */ +inline fun Response.parseAsIsland(json: Json = islandJson): T = parseAs(json) { extractIsland(it) } diff --git a/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/MtlArabic.kt b/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/MtlArabic.kt new file mode 100644 index 000000000..572077d93 --- /dev/null +++ b/src/ar/mtlarabic/src/eu/kanade/tachiyomi/extension/ar/mtlarabic/MtlArabic.kt @@ -0,0 +1,201 @@ +package eu.kanade.tachiyomi.novelextension.ar.mtlarabic + +import eu.kanade.tachiyomi.source.NovelSource +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.model.SMangaUpdate +import keiyoushi.annotation.Source +import keiyoushi.network.get +import keiyoushi.source.KeiSource +import keiyoushi.utils.parseAs +import kotlinx.serialization.json.JsonElement +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl + +private const val SORT_BY_LATEST = "latest_update" +private const val SORT_BY_CHAPTERS = "-num_chapters" + +/** + * مكتبة الخيال (mtlarabic.com) — an Arabic web-novel library. + * + * A server-rendered Node app. `/novels`, `/novel-details` and the reader answer `text/html` with + * their data in a `