diff --git a/src/en/rudrascans/AndroidManifest.xml b/src/en/rudrascans/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/src/en/rudrascans/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/en/rudrascans/build.gradle b/src/en/rudrascans/build.gradle new file mode 100644 index 000000000..a305b2473 --- /dev/null +++ b/src/en/rudrascans/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Rudra Scans' + pkgNameSuffix = 'en.rudrascans' + extClass = '.RudraScans' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/rudrascans/res/mipmap-hdpi/ic_launcher.png b/src/en/rudrascans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..dca8c86d7 Binary files /dev/null and b/src/en/rudrascans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/rudrascans/res/mipmap-mdpi/ic_launcher.png b/src/en/rudrascans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..1a3e2ab24 Binary files /dev/null and b/src/en/rudrascans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/rudrascans/res/mipmap-xhdpi/ic_launcher.png b/src/en/rudrascans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..cd02f02c4 Binary files /dev/null and b/src/en/rudrascans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/rudrascans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/rudrascans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..40c2639bc Binary files /dev/null and b/src/en/rudrascans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/rudrascans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/rudrascans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..c39f1d3b9 Binary files /dev/null and b/src/en/rudrascans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/rudrascans/res/web_hi_res_512.png b/src/en/rudrascans/res/web_hi_res_512.png new file mode 100644 index 000000000..6fd1da9f7 Binary files /dev/null and b/src/en/rudrascans/res/web_hi_res_512.png differ diff --git a/src/en/rudrascans/src/eu/kanade/tachiyomi/extension/en/rudrascans/RudraScans.kt b/src/en/rudrascans/src/eu/kanade/tachiyomi/extension/en/rudrascans/RudraScans.kt new file mode 100644 index 000000000..fa53cb677 --- /dev/null +++ b/src/en/rudrascans/src/eu/kanade/tachiyomi/extension/en/rudrascans/RudraScans.kt @@ -0,0 +1,178 @@ +package eu.kanade.tachiyomi.extension.en.rudrascans + +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.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +class RudraScans : ParsedHttpSource() { + + override val name = "Rudra Scans" + + override val baseUrl = "https://rudrascans.com" + + override val lang = "en" + + override val supportsLatest = true + + override val client = super.client.newBuilder() + .rateLimit(1) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + // Popular + + override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers) + + override fun popularMangaSelector(): String = "div.flex-col div.grid > div.group.border" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + thumbnail_url = element.getImageUrl("*[style*=background-image]") + element.selectFirst("a[href]")!!.run { + title = attr("title") + setUrlWithoutDomain(attr("href")) + } + } + + override fun popularMangaNextPageSelector(): String? = null + + // Latest + + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest/", headers) + + override fun latestUpdatesSelector(): String = "div.grid > div.group" + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector(): String? = null + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("series") + addPathSegment("") + addQueryParameter("q", query) + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = "#searched_series_page > a" + + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + thumbnail_url = element.getImageUrl("*[style*=background-image]") + title = element.attr("title") + setUrlWithoutDomain(element.attr("href")) + } + + override fun searchMangaNextPageSelector(): String? = null + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.use { it.asJsoup() } + val query = response.request.url.queryParameter("q")!! + + val mangaList = document.select(searchMangaSelector()) + .map(::searchMangaFromElement) + .filter { it.title.contains(query, true) } + + return MangasPage(mangaList, false) + } + + // Details + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + title = document.selectFirst("div.grid > h1")!!.text() + thumbnail_url = document.getImageUrl("div[class*=photoURL]") + description = document.selectFirst("div.grid > div.overflow-hidden > p")?.text() + status = document.selectFirst("div[alt=Status]").parseStatus() + genre = document.select("div.grid:has(>h1) > div.flex > div.leading-none:not([alt])").joinToString(", ") { + it.text().trim() + } + } + + private fun Element?.parseStatus(): Int = when (this?.text()?.trim()) { + "ongoing" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + + // Chapter list + + override fun chapterListSelector(): String = "#chapters > a" + + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href")) + name = element.selectFirst(".text-sm")!!.text() + element.selectFirst(".text-xs")?.run { + date_upload = text().trim().parseDate() + } + } + + // Image list + + override fun pageListParse(document: Document): List { + return document.select("#pages > img").map { + val index = it.attr("count").toInt() + Page(index, document.location(), it.imgAttr("150")) + } + } + + override fun imageUrlParse(document: Document) = "" + + // Utilities + + // From mangathemesia + private fun Element.imgAttr(width: String): String { + val url = when { + hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") + hasAttr("data-src") -> attr("abs:data-src") + else -> attr("abs:src") + } + return url.toHttpUrl() + .newBuilder() + .addQueryParameter("w", width) + .build() + .toString() + } + + private fun Element.getImageUrl(selector: String): String? { + return this.selectFirst(selector)?.let { + it.attr("style") + .substringAfter(":url(", "") + .substringBefore(")", "") + .takeIf { it.isNotEmpty() } + ?.toHttpUrlOrNull()?.let { + it.newBuilder() + .setQueryParameter("w", "480") + .build() + .toString() + } + } + } + + private fun String.parseDate(): Long { + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L + } + + companion object { + private val DATE_FORMATTER by lazy { + SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH) + } + } +}